[
  {
    "path": ".asf.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# NOTE: All configurations could be found here: https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features\ngithub:\n  description: \"A Q&A platform software for teams at any scales. Whether it's a community forum, help center, or knowledge management platform, you can always count on Apache Answer.\"\n  homepage: https://answer.apache.org\n  labels:\n    - react\n    - go\n    - golang\n    - community\n    - forum\n    - question\n    - typescript\n    - q-and-a\n    - hacktoberfest\n  features:\n    wiki: true\n    issues: true\n    projects: true\n    discussions: false\n  enabled_merge_buttons:\n    squash: true\n    rebase: true\n    merge: false\n  protected_branches:\n    main: {}\n\nnotifications:\n  commits:      commits@answer.apache.org\n  issues:       issues@answer.apache.org\n  pullrequests: issues@answer.apache.org\n  discussions:  issues@answer.apache.org\n"
  },
  {
    "path": ".editorconfig",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.go]\nindent_style = tab\nindent_size = 2\n\n[{Makefile, Dockerfile}]\nindent_style = tab\nindent_size = 4\n\n[{*.yml, *.json}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report an issue to help the project improve.\ntitle: ''\nlabels: bug\ntype: 'Bug'\nassignees: ''\n\n---\n\n## Describe the bug\n\nA clear and concise description of what the bug is.\n\n## To Reproduce\n\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n## Expected behavior\n\nA clear and concise description of what you expected to happen.\n\n## Screenshots\n\nIf applicable, add screenshots or video to help explain your problem.\n\n## Platform\n\n- Device: [e.g. Desktop, Mobile]\n- OS: [e.g. macOS]\n- Browser and version: [e.g. Chrome, Safari]\n- Version: [e.g. v1.2.0]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nblank_issues_enabled: true\ncontact_links:\n  - name: Questions & Discussions\n    url: https://meta.answer.dev\n    about: If you have any questions while using.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement_request.md",
    "content": "---\nname: Enhancement request\nabout: Suggest an enhancement for this project. Improve an existing feature.\ntitle: ''\nlabels: enhancement\ntype: 'Feature'\nassignees: ''\n\n---\n\n## Is your enhancement request related to a problem? Please describe\n\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n## Describe the solution you'd like\n\nA clear and concise description of what you want to happen.\n\n## Describe alternatives you've considered\n\nA clear and concise description of any alternative solutions or features you've considered.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea or possible new feature for this project.\ntitle: ''\nlabels: new-feature\ntype: 'Feature'\nassignees: ''\n\n---\n\n## Is your feature request related to a problem? Please describe\n\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n## Describe the solution you'd like\n\nA clear and concise description of what you want to happen.\n\n## Describe alternatives you've considered\n\nA clear and concise description of any alternative solutions or features you've considered.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md",
    "content": "Fixes #\n\n## Proposed Changes\n\n  -\n  -\n  -\n"
  },
  {
    "path": ".github/workflows/build-binary-for-release.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Build Binary For Release\n\non:\n  push:\n    tags:\n      - \"v*\"\npermissions:\n  contents: write\n\njobs:\n  build-goreleaser:\n    if: ${{ github.repository_owner == 'apache' }}\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Set up Node\n      uses: actions/setup-node@v3\n      with:\n        node-version: 20.18.1\n\n    - name: Node Build\n      run: make install-ui-packages ui\n\n    - name: Setup Go\n      uses: actions/setup-go@v3\n      with:\n        go-version: 1.23\n    - name: Run GoReleaser\n      uses: goreleaser/goreleaser-action@v4\n      with:\n        distribution: goreleaser\n        version: latest\n        args: release --clean --skip=validate\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    - uses: actions/upload-artifact@v4\n      with:\n        name: answer\n        path: ./dist/*\n"
  },
  {
    "path": ".github/workflows/build-image-for-latest-release.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Build Latest Docker Image For Release\n\non:\n  push:\n    tags:\n      - v2.*\n      - v1.*\n      - v0.*\n      - \"!v*-RC*\"\n  # pull_request:\n  #   branches: [ \"main\" ]\n\njobs:\n  build:\n    if: ${{ github.repository_owner == 'apache' }}\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: apache/answer\n        tags: |\n          type=raw,value=latest\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v2\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v2\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USER }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Build and push\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        platforms: linux/amd64,linux/arm64\n        push: true\n        file: ./Dockerfile\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n\n\n"
  },
  {
    "path": ".github/workflows/build-image-for-manual.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Manual Build Docker Image For Release\n\non:\n  workflow_dispatch:\n    inputs:\n      tag_name:\n        type: string\n        required: true\n        description: 'DockerHub img tag name'\n\njobs:\n  build:\n    if: ${{ github.repository_owner == 'apache' }}\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: apache/answer\n        tags: |\n          type=ref,enable=true,priority=600,prefix=,suffix=,event=branch\n          type=semver,pattern={{version}}\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v2\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v2\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USER }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Build and push\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        platforms: linux/amd64,linux/arm64\n        push: true\n        file: ./Dockerfile\n        tags: apache/answer:${{ inputs.tag_name }}\n        labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/build-image-for-release.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Build Docker Image For Release\n\non:\n  push:\n    tags:\n      - v2.*\n      - v1.*\n      - v0.*\n  # pull_request:\n  #   branches: [ \"main\" ]\n\njobs:\n  build:\n    if: ${{ github.repository_owner == 'apache' }}\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: apache/answer\n        tags: |\n          type=ref,enable=true,priority=600,prefix=,suffix=,event=branch\n          type=semver,pattern={{version}}\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v2\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v2\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USER }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Build and push\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        platforms: linux/amd64,linux/arm64\n        push: true\n        file: ./Dockerfile\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n\n\n"
  },
  {
    "path": ".github/workflows/build-image-for-test.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Build Docker Image For Test\n\non:\n  push:\n    branches: [ \"test\" ]\n\njobs:\n  build:\n    if: ${{ github.repository_owner == 'apache' }}\n    name: Build and Push\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@v4\n      with:\n        images: apache/answer\n        tags: |\n          type=raw,value=test\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v2\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v2\n\n    - name: Login to DockerHub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USER }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n    - name: Build and push\n      uses: docker/build-push-action@v4\n      with:\n        context: .\n        file: ./Dockerfile\n        platforms: linux/amd64\n        push: true\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/check-asf-header.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: CI\non:\n  pull_request:\n    branches: [ main ]\n  push:\n    branches: [ main ]\n\n# Concurrency strategy:\n#   github.workflow: distinguish this workflow from others\n#   github.event_name: distinguish `push` event from `pull_request` event\n#   github.event.number: set to the number of the pull request if `pull_request` event\n#   github.run_id: otherwise, it's a `push` or `schedule` event, only cancel if we rerun the workflow\n#\n# Reference:\n#   https://docs.github.com/en/actions/using-jobs/using-concurrency\n#   https://docs.github.com/en/actions/learn-github-actions/contexts#github-context\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  check:\n    name: Check and lint\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n      - name: Check license header\n        uses: korandoru/hawkeye@v3\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nname: Lint\n\non:\n  push:\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    name: Lint (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 15\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.23\"\n          cache: true\n\n      - name: Run go mod tidy\n        run: go mod tidy\n\n      - name: Run golangci-lint\n        run: make lint\n\n      - name: Check for uncommitted changes\n        shell: bash\n        run: |\n          if [ -n \"$(git status --porcelain)\" ]; then\n            echo \"::error::Uncommitted changes detected\"\n            git status\n            git diff\n            exit 1\n          fi\n"
  },
  {
    "path": ".gitignore",
    "content": "*.exe\n*.orig\n*.rej\n*.so\n*~\n*.db\n.DS_Store\n._*\n/.idea\n/.fleet\n/.vscode/*.log\n/cmd/answer/*.sh\n/cmd/answer/answer\n/cmd/answer/uploads/*\n/cmd/logs\n/configs/config-dev.yaml\n/go.work*\n/logs\n/ui/node_modules\n/ui/build/*/*/*\n/ui/build/*.json\n/ui/build/*.html\n/ui/build/*.txt\n/vendor\nThumbs*.db\ntmp\nvendor/\n/answer-data/\n/answer\n/new_answer\nbuild/tools/\ndist/\n\n# Lint setup generated file\n.husky/\n\n# Environment variables\n.env"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\ninclude:\n  - project: \"segmentfault/devops/templates\"\n    file: \".docker-build-push.yml\"\n  - project: \"segmentfault/devops/templates\"\n    file: \".deploy-helm.yml\"\n\nstages:\n  - deploy-dev\n\n\"deploy-to-local-develop-environment\":\n  stage: deploy-dev\n  extends: .deploy-helm\n  only:\n    - test\n  variables:\n    LoadBalancerIP: 10.0.10.98\n    KubernetesCluster: dev\n    KubernetesNamespace: \"sf-web\"\n    InstallArgs: --set service.loadBalancerIP=${LoadBalancerIP} --set image.tag=latest --set replicaCount=1 --set serivce.targetPort=80\n    ChartName: answer\n    InstallPolicy: replace\n\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nversion: \"2\"\nlinters:\n  exclusions:\n    paths:\n      - answer-data\n      - ui\n      - i18n\n  enable:\n    - asasalint # checks for pass []any as any in variadic func(...any)\n    - asciicheck # checks that your code does not contain non-ASCII identifiers\n    - bidichk # checks for dangerous unicode character sequences\n    - bodyclose # checks whether HTTP response body is closed successfully\n    - canonicalheader # checks whether net/http.Header uses canonical header\n    - copyloopvar # detects places where loop variables are copied (Go 1.22+)\n    - gocritic # provides diagnostics that check for bugs, performance and style issues\n    - misspell # finds commonly misspelled English words in comments and strings\n    - modernize # detects code that can be modernized to use newer Go features\n    - testifylint # checks usage of github.com/stretchr/testify\n    - unconvert # removes unnecessary type conversions\n    - unparam # reports unused function parameters\n    - whitespace # detects leading and trailing whitespace\n\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  settings:\n    gofmt:\n      simplify: true\n      rewrite-rules:\n        - pattern: 'interface{}'\n          replacement: 'any'\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nversion: 2\n\nenv:\n  - GO11MODULE=on\n  - GO111MODULE=on\n  - GOPROXY=https://goproxy.io,direct\n  - CGO_ENABLED=0\n\nbefore:\n  hooks:\n    - go mod tidy\n\nrelease:\n  draft: true\n\nbuilds:\n  - id: build\n    main: ./cmd/answer/.\n    binary: answer\n    ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser\n    flags: -v\n    goos:\n      - linux\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n  - id: build-windows\n    main: ./cmd/answer/.\n    binary: answer\n    ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser\n    flags: -v\n    goos:\n      - windows\n    goarch:\n      - amd64\n\n\n\n\narchives:\n  - name_template: >-\n      apache-answer-{{ .RawVersion }}-bin-{{ .Os }}-{{ .Arch }}\n    files:\n      - src: \"docs/release/LICENSE\"\n        dst: LICENSE\n      - src: \"docs/release/NOTICE\"\n        dst: NOTICE\n      - src: \"docs/release/licenses/*\"\n        dst: licenses/\n    wrap_in_directory: true\nchecksum:\n  name_template: 'checksums.txt'\nsnapshot:\n  version_template: \"{{ incpatch .Version }}\"\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - '^docs:'\n      - '^test:'\n\nsource:\n  enabled: true\n  name_template: apache-answer-{{ .RawVersion }}-src\n  prefix_template: \"apache-answer-{{ .RawVersion }}-src/\"\n\n# goreleaser release --skip-validate  --skip-publish --clean\n\n"
  },
  {
    "path": ".vaunt/config.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nversion: 0.0.1\nachievements:\n  - achievement:\n      name: Visionary Architect\n      icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/enhancement.png\n      description: Awarded for bringing up enhancement, dream big!\n      triggers:\n        - trigger:\n            actor: assignees\n            action: issue\n            condition: labels in ['enhancement'] & labels in ['LGTM']\n  - achievement:\n      name: Bug Hunter\n      icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/bug.png\n      description: Awarded for identifying real bugs, well spotted!\n      triggers:\n        - trigger:\n            actor: assignees\n            action: issue\n            condition: labels in ['bug'] & labels in ['LGTM']\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.workingDirectories\": [\n    \"ui\"\n  ],\n  \"explorer.autoReveal\": \"focusNoScroll\",\n  \"cSpell.words\": [\n    \"grecaptcha\"\n  ]\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nFROM golang:1.24-alpine AS golang-builder\nLABEL maintainer=\"linkinstar@apache.org\"\n\nARG GOPROXY\n# ENV GOPROXY ${GOPROXY:-direct}\n# ENV GOPROXY=https://proxy.golang.com.cn,direct\n\nENV GOPATH /go\nENV GOROOT /usr/local/go\nENV PACKAGE github.com/apache/answer\nENV BUILD_DIR ${GOPATH}/src/${PACKAGE}\nENV ANSWER_MODULE ${BUILD_DIR}\n\nARG TAGS=\"sqlite sqlite_unlock_notify\"\nENV TAGS \"bindata timetzdata $TAGS\"\nARG CGO_EXTRA_CFLAGS\n\nCOPY . ${BUILD_DIR}\nWORKDIR ${BUILD_DIR}\nRUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm@9.7.0 \\\n    && make clean build\n\nRUN chmod 755 answer\nRUN [\"/bin/bash\",\"-c\",\"script/build_plugin.sh\"]\nRUN cp answer /usr/bin/answer\n\nRUN mkdir -p /data/uploads && chmod 777 /data/uploads \\\n    && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n\n\nFROM alpine\nLABEL maintainer=\"linkinstar@apache.org\"\n\nARG TIMEZONE\nENV TIMEZONE=${TIMEZONE:-\"Asia/Shanghai\"}\n\nRUN apk update \\\n    && apk --no-cache add \\\n        bash \\\n        ca-certificates \\\n        curl \\\n        dumb-init \\\n        gettext \\\n        openssh \\\n        sqlite \\\n        gnupg \\\n        tzdata \\\n    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \\\n    && echo \"${TIMEZONE}\" > /etc/timezone\n\nCOPY --from=golang-builder /usr/bin/answer /usr/bin/answer\nCOPY --from=golang-builder /data /data\nCOPY /script/entrypoint.sh /entrypoint.sh\nRUN chmod 755 /entrypoint.sh\n\nVOLUME /data\nEXPOSE 80\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build clean ui\n\nVERSION=2.0.0\nBIN=answer\nDIR_SRC=./cmd/answer\nDOCKER_CMD=docker\n\nGO_ENV=CGO_ENABLED=0 GO111MODULE=on\nRevision=$(shell git rev-parse --short HEAD 2>/dev/null || echo \"\")\nGO_FLAGS=-ldflags=\"-X github.com/apache/answer/cmd.Version=$(VERSION) -X 'github.com/apache/answer/cmd.Revision=$(Revision)' -X 'github.com/apache/answer/cmd.Time=`date +%s`' -extldflags -static\"\nGO=$(GO_ENV) \"$(shell which go)\"\n\nGOLANGCI_VERSION ?= v2.6.2\nTOOLS_BIN := $(shell mkdir -p build/tools && realpath build/tools)\n\nGOLANGCI = $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION)\n$(GOLANGCI):\n\trm -f $(TOOLS_BIN)/golangci-lint*\n\tcurl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_VERSION)/install.sh | sh -s -- -b $(TOOLS_BIN) $(GOLANGCI_VERSION)\n\tmv $(TOOLS_BIN)/golangci-lint $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION)\n\nbuild: generate\n\t@$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)\n\n# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3\nuniversal: generate\n\t@GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC)\n\t@GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC)\n\t@lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64\n\t@rm -f ${BIN}_amd64 ${BIN}_arm64\n\ngenerate:\n\t@$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3\n\t@$(GO) get github.com/google/wire/cmd/wire@v0.5.0\n\t@$(GO) get go.uber.org/mock/mockgen@v0.6.0\n\t@$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3\n\t@$(GO) install github.com/google/wire/cmd/wire@v0.5.0\n\t@$(GO) install go.uber.org/mock/mockgen@v0.6.0\n\t@$(GO) generate ./...\n\t@$(GO) mod tidy\n\ncheck:\n\t@mockgen -version\n\t@swag -v\n\t@wire flags\n\ntest:\n\t@$(GO) test ./internal/repo/repo_test\n\n# clean all build result\nclean:\n\t@$(GO) clean ./...\n\t@rm -f $(BIN)\n\ninstall-ui-packages:\n\t@corepack enable\n\t@corepack prepare pnpm@9.7.0 --activate\n\nui:\n\t@cd ui && pnpm pre-install && pnpm build && cd -\n\nlint: generate $(GOLANGCI)\n\t@bash ./script/check-asf-header.sh\n\t$(GOLANGCI) run\n\nlint-fix: generate $(GOLANGCI)\n\t@bash ./script/check-asf-header.sh\n\t$(GOLANGCI) run --fix\n\nall: clean build\n"
  },
  {
    "path": "NOTICE",
    "content": "Apache Answer\nCopyright 2023-2025 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (https://www.apache.org/).\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://answer.apache.org\">\n    <img alt=\"logo\" src=\"docs/img/logo.svg\" height=\"99px\">\n</a>\n\n# Apache Answer - Build Q&A platform\n\nA Q&A platform software for teams at any scales. Whether it’s a community forum, help center, or knowledge management platform, you can always count on Answer.\n\nTo learn more about the project, visit [answer.apache.org](https://answer.apache.org).\n\n[![LICENSE](https://img.shields.io/github/license/apache/answer)](https://github.com/apache/answer/blob/main/LICENSE)\n[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/)\n[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/)\n[![Go Report Card](https://goreportcard.com/badge/github.com/apache/answer)](https://goreportcard.com/report/github.com/apache/answer)\n[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej)\n\n## Screenshots\n\n![screenshot](docs/img/screenshot.png)\n\n## Quick start\n\n### Running with docker\n\n```bash\ndocker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:2.0.0\n```\n\nFor more information, see [Installation](https://answer.apache.org/docs/installation).\n\n### Plugins\n\nAnswer provides a plugin system for developers to create custom plugins and expand Answer’s features. You can find the [plugin documentation here](https://answer.apache.org/community/plugins).\n\nWe value your feedback and suggestions to improve our documentation. If you have any comments or questions, please feel free to contact us. We’re excited to see what you can create using our plugin system!\n\nYou can also check out the [plugins here](https://answer.apache.org/plugins).\n\n## Building from Source\n\n### Prerequisites\n\n- Golang >= 1.23\n- Node.js >= 20\n- pnpm >= 9\n- [mockgen](https://github.com/uber-go/mock?tab=readme-ov-file#installation) >= 0.6.0\n- [wire](https://github.com/google/wire/) >= 0.5.0\n\n### Build\n\n```bash\n# Install wire and mockgen for building. You can run `make check` to check if they are installed.\n$ make generate\n# Install frontend dependencies and build\n$ make ui\n# Install backend dependencies and build\n$ make build\n```\n\n## Contributing\n\nContributions are always welcome!\n\nSee [CONTRIBUTING](https://answer.apache.org/community/contributing) for ways to get started.\n\n## License\n\n[Apache License 2.0](https://github.com/apache/answer/blob/main/LICENSE)\n"
  },
  {
    "path": "build/README.md",
    "content": "# /build\nPackaging and Continuous Integration.\n"
  },
  {
    "path": "charts/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "charts/Chart.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\napiVersion: v2\nname: answer\ndescription: A simple answer deployments for kubernetes\ntype: application\nversion: 0.1.0\nappVersion: \"1.0.7\""
  },
  {
    "path": "charts/README.md",
    "content": "# answer\n\nAn open-source knowledge-based community software. You can use it quickly to build Q&A community for your products, customers, teams, and more.\n## Prerequisites\n\n- Kubernetes 1.20+\n## Configuration\n\nThe following table lists the configurable parameters of the answer chart and their default values.\n\n| Parameter | Description | Default |\n| --------- | ----------- | ------- |\n| `replicaCount`  | Number of answer replicas  | `1` |\n| `image.repository` | Image repository | `apache/answer` |\n| `image.pullPolicy` | Image pull policy | `Always` |\n| `image.tag` | Image tag | `latest` |\n| `env` | Optional environment variables for answer | `LOG_LEVEL: INFO` |\n| `extraContainers` | Optional sidecar containers to run along side answer | `[]` |\n| `persistence.enabled` | Enable or disable persistence for the /data volume | `true` |\n| `persistence.accessMode` | Specify the access mode of the persistent volume | `ReadWriteOnce` |\n| `persistence.size` | The size of the persistent volume | `5Gi` |\n| `persistence.annotations` | Annotations to add to the volume claim | `{}` |\n| `imagePullSecrets` | Reference to one or more secrets to be used when pulling images | `[]` |\n| `nameOverride` | nameOverride replaces the name of the chart in the Chart.yaml file, when this is used to construct Kubernetes object names. |  |\n| `fullnameOverride` | fullnameOverride completely replaces the generated name. |  |\n| `serviceAccount.create` | If `true`, create a new service account | `true` |\n| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |\n| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template |  |\n| `podAnnotations` | Annotations to add to the answer pod | `{}` |\n| `podSecurityContext` | Security context for the answer pod | `{}` refer to [Default Security Contexts](#default-security-contexts) |\n| `securityContext` | Security context for the answer container | `{}` refer to [Default Security Contexts](#default-security-contexts) |\n| `service.type` | The type of service to be used | `ClusterIP` |\n| `service.port` | The port that the service should listen on for requests. Also used as the container port. | `80` |\n| `ingress.enabled` | Enable or disable ingress. | `false` |\n| `resources` | CPU/memory resource requests/limits | `{}` |\n| `autoscaling.enabled` | Enable or disable pod autoscaling. If enabled, replicas are disabled. | `false` |\n| `nodeSelector` | Node labels for pod assignment | `{}` |\n| `tolerations` | Node tolerations for pod assignment | `[]` |\n| `affinity` | Node affinity for pod assignment | `{}` |\n\n### Default Security Contexts\n\nThe default pod-level and container-level security contexts, below, adhere to the [restricted](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted) Pod Security Standards policies.\n\nDefault pod-level securityContext:\n```yaml\nrunAsNonRoot: true\nseccompProfile:\n  type: RuntimeDefault\n```\n\nDefault containerSecurityContext:\n```yaml\nallowPrivilegeEscalation: false\ncapabilities:\n  drop:\n  - ALL\n```\n### Installing with a Values file\n\n```console\n$ helm install answer -f values.yaml .\n```\n> **Tip**: You can use the default [values.yaml]\n\n## TODO\n\nPublish the chart to Artifacthub and add proper installation instructions. E.G.\n> **NOTE**: This is not currently a valid installation option.\n\n```console\n$ helm repo add apache https://charts.answer.apache.org/\n$ helm repo update\n$ helm install apache/answer -n mynamespace\n```"
  },
  {
    "path": "charts/templates/_helpers.tpl",
    "content": "{{/*\n\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements.  See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership.  The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n\"License\"); you may not use this file except in compliance\nwith the License.  You may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied.  See the License for the\nspecific language governing permissions and limitations\nunder the License.\n\n*/}}\n{{- define \"answer.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"answer.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"answer.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"answer.labels\" -}}\nhelm.sh/chart: {{ include \"answer.chart\" . }}\n{{ include \"answer.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"answer.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"answer.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"answer.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"answer.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/templates/deployment.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"answer.fullname\" . }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\nspec:\n  {{- if not .Values.autoscaling.enabled }}\n  replicas: {{ .Values.replicaCount }}\n  {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"answer.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"answer.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"answer.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.port }}\n              protocol: TCP\n          livenessProbe:\n            httpGet:\n              path: /\n              port: http\n          readinessProbe:\n            httpGet:\n              path: /\n              port: http\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n          {{- if .Values.env }}\n          env:\n            {{- range .Values.env }}\n            - name: {{ .name }}\n              {{- if .value | quote }}\n              value: {{ .value | quote }}\n              {{- end }}\n              {{- if .valueFrom }}\n              valueFrom:\n                {{- toYaml .valueFrom | nindent 16 }}\n              {{- end }}\n            {{- end }}\n          {{- end }}\n          volumeMounts:\n            - name: data\n              mountPath: \"/data\"\n      {{- if .Values.extraContainers }}\n        {{- toYaml .Values.extraContainers | nindent 8 }}\n      {{- end }}\n      volumes:\n        - name: data\n          {{- if .Values.persistence.enabled }}\n          persistentVolumeClaim:\n            claimName: {{ include \"answer.fullname\" . }}-claim\n          {{- else }}\n          emptyDir: {}\n          {{- end -}}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "charts/templates/hpa.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n{{ if .Values.autoscaling.enabled -}}\napiVersion: autoscaling/v2beta1\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ include \"answer.fullname\" . }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ include \"answer.fullname\" . }}\n  minReplicas: {{ .Values.autoscaling.minReplicas }}\n  maxReplicas: {{ .Values.autoscaling.maxReplicas }}\n  metrics:\n    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: cpu\n        targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}\n    {{- end }}\n    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: memory\n        targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/templates/ingress.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n{{ if .Values.ingress.enabled -}}\n{{- $fullName := include \"answer.fullname\" . -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if and .Values.ingress.className (not (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion)) }}\n  {{- if not (hasKey .Values.ingress.annotations \"kubernetes.io/ingress.class\") }}\n  {{- $_ := set .Values.ingress.annotations \"kubernetes.io/ingress.class\" .Values.ingress.className}}\n  {{- end }}\n{{- end }}\n{{- if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n{{- else if semverCompare \">=1.14-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1beta1\n{{- else -}}\napiVersion: extensions/v1beta1\n{{- end }}\nkind: Ingress\nmetadata:\n  name: {{ $fullName }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if and .Values.ingress.className (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion) }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            {{- if and .pathType (semverCompare \">=1.18-0\" $.Capabilities.KubeVersion.GitVersion) }}\n            pathType: {{ .pathType }}\n            {{- end }}\n            backend:\n              {{- if semverCompare \">=1.19-0\" $.Capabilities.KubeVersion.GitVersion }}\n              service:\n                name: {{ $fullName }}\n                port:\n                  number: {{ $svcPort }}\n              {{- else }}\n              serviceName: {{ $fullName }}\n              servicePort: {{ $svcPort }}\n              {{- end }}\n          {{- end }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/templates/pvc.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n{{ if .Values.persistence.enabled -}}\nkind: PersistentVolumeClaim\napiVersion: v1\nmetadata:\n  name: {{ include \"answer.fullname\" . }}-claim\n  {{- with .Values.persistence.annotations }}\n  annotations:\n    {{ toYaml . | indent 4 }}\n  {{- end }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\nspec:\n  {{- if .Values.persistence.storageClass }}\n  {{- if (eq \"-\" .Values.persistence.storageClass) }}\n  storageClassName: \"\"\n  {{- else }}\n  storageClassName: \"{{ .Values.persistence.storageClass }}\"\n  {{- end }}\n  {{- end }}\n  accessModes:\n    - {{ .Values.persistence.accessMode | quote }}\n  resources:\n    requests:\n      storage: {{ .Values.persistence.size | quote }}\n  {{- with .Values.persistence.dataSource }}\n  dataSource:\n    name: {{ .name }}\n    kind: {{ .kind | default \"VolumeSnapshot\" }}\n    apiGroup: {{ .apiGroup | default \"snapshot.storage.k8s.io\" }}\n  {{- end }}\n{{- end }}"
  },
  {
    "path": "charts/templates/service.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"answer.fullname\" . }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"answer.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "charts/templates/serviceaccount.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n{{ if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"answer.serviceAccountName\" . }}\n  labels:\n    {{- include \"answer.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/values.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# Default values for answer.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\n\nimage:\n  repository: apache/answer\n  pullPolicy: Always\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"latest\"\n\n# Environment variables\n# Configure environment variables below\n# https://answer.apache.org/docs/env\nenv:\n  - name: LOG_LEVEL\n    # [DEBUG INFO WARN ERROR]\n    value: \"INFO\"\n  # uncomment the below values to use AUTO_INSTALL and not have to go through the setup process.\n  # Once used to do the initial setup, these variables won't be used moving forward.\n  # You must at a minimum comment AUTO_INSTALL after initial setup to prevent an error about the database already being initiated.\n  # - name: AUTO_INSTALL\n  #   value: \"true\"\n  # - name: DB_TYPE\n  #   value: \"sqlite3\"\n  #   # DB_FILE Only for sqlite3\n  # - name: DB_FILE\n  #   value: \"/data/answer.db\"\n  # - name: LANGUAGE\n  #   value: \"en-US\"\n  # - name: SITE_NAME\n  #   value: \"MyAnswer\"\n  # - name: SITE_URL\n  #   value: \"http://localhost:80\"\n  # - name: CONTACT_EMAIL\n  #   value: \"support@mydomain.com\"\n  # - name: ADMIN_NAME\n  #   # lowercase\n  #   value: \"myadmin\"\n  # - name: ADMIN_PASSWORD\n  #   # 32 Characters MAX\n  #   value: \"MyInsecurePasswordInTheRepo!\"\n  #   # Use valueFrom to use a secret\n  #   # valueFrom:\n  #   #   secretKeyRef:\n  #   #     key: answer-admin-password\n  #   #     name: answer-secrets\n  # - name: ADMIN_EMAIL\n  #   value: \"myAdmin@mydomain.com\"\n\n# Configure extra containers\nextraContainers: []\n  # - name: cloudsql-proxy\n  #   image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2\n  #   command:\n  #     - /cloud-sql-proxy\n  #   args:\n  #     - project:region:instance\n  #     - --port=5432\n  #     - --auto-iam-authn\n  #   ports:\n  #     - containerPort: 5432\n\n# Persistence for the /data volume\n# Without persistence, your uploads and config.yaml will not be remembered between restarts.\npersistence:\n  enabled: true\n  # If set to \"-\", storageClassName: \"\", which disables dynamic provisioning\n  # If undefined (the default) or set to null, no storageClassName spec is\n  #  set, choosing the default provisioner.  (gp2 on AWS, standard on\n  #  GKE, AWS & OpenStack)\n  # storageClass: \"-\"\n  accessMode: ReadWriteOnce\n  size: 5Gi\n  annotations: {}\n  # To restore a PVC from a VolumeSnapshot, set the dataSource;\n  # the kind and apiGroup are optional and default to the shown values\n  dataSource: {}\n    # name: my-volume-snapshot\n    # kind: VolumeSnapshot\n    # apiGroup: snapshot.storage.k8s.io\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name: \"\"\n\npodAnnotations: {}\n\npodSecurityContext: {}\n  # fsGroup: 2000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  type: ClusterIP\n  port: 80\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: answer.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n  #  - secretName: answer-tls\n  #    hosts:\n  #      - answer.local\n\nresources: {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 100\n  targetCPUUtilizationPercentage: 80\n  # targetMemoryUtilizationPercentage: 80\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}"
  },
  {
    "path": "cmd/answer/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n//go:generate go run github.com/swaggo/swag/cmd/swag init -g ./cmd/answer/main.go -d ../../ -o ../../docs\n\npackage main\n\nimport (\n\tanswercmd \"github.com/apache/answer/cmd\"\n)\n\n// main godoc\n// @title Apache Answer\n// @description Apache Answer API\n// @BasePath /\n// @securityDefinitions.apikey ApiKeyAuth\n// @in header\n// @name Authorization\nfunc main() {\n\tanswercmd.Main()\n}\n"
  },
  {
    "path": "cmd/command.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage answercmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/cli\"\n\t\"github.com/apache/answer/internal/install\"\n\t\"github.com/apache/answer/internal/migrations\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\t// dataDirPath save all answer application data in this directory. like config file, upload file...\n\tdataDirPath string\n\t// dumpDataPath dump data path\n\tdumpDataPath string\n\t// place to build new answer\n\tbuildDir string\n\t// plugins needed to build in answer application\n\tbuildWithPlugins []string\n\t// build output path\n\tbuildOutput string\n\t// This config is used to upgrade the database from a specific version manually.\n\t// If you want to upgrade the database to version 1.1.0, you can use `answer upgrade -f v1.1.0`.\n\tupgradeVersion string\n\t// The fields that need to be set to the default value\n\tconfigFields []string\n\t// i18nSourcePath i18n from path\n\ti18nSourcePath string\n\t// i18nTargetPath i18n to path\n\ti18nTargetPath string\n\t// resetPasswordEmail user email for password reset\n\tresetPasswordEmail string\n\t// resetPasswordPassword new password for password reset\n\tresetPasswordPassword string\n)\n\nfunc init() {\n\trootCmd.Version = fmt.Sprintf(\"%s\\nrevision: %s\\nbuild time: %s\", Version, Revision, Time)\n\n\trootCmd.PersistentFlags().StringVarP(&dataDirPath, \"data-path\", \"C\", \"/data/\", \"data path, eg: -C ./data/\")\n\n\tdumpCmd.Flags().StringVarP(&dumpDataPath, \"path\", \"p\", \"./\", \"dump data path, eg: -p ./dump/data/\")\n\n\tbuildCmd.Flags().StringSliceVarP(&buildWithPlugins, \"with\", \"w\", []string{}, \"plugins needed to build\")\n\n\tbuildCmd.Flags().StringVarP(&buildOutput, \"output\", \"o\", \"\", \"build output path\")\n\n\tbuildCmd.Flags().StringVarP(&buildDir, \"build-dir\", \"b\", \"\", \"dir for build process\")\n\n\tupgradeCmd.Flags().StringVarP(&upgradeVersion, \"from\", \"f\", \"\", \"upgrade from specific version, eg: -f v1.1.0\")\n\n\tconfigCmd.Flags().StringSliceVarP(&configFields, \"with\", \"w\", []string{}, \"the fields that need to be set to the default value, eg: -w allow_password_login\")\n\n\ti18nCmd.Flags().StringVarP(&i18nSourcePath, \"source\", \"s\", \"\", \"i18n source path, eg: -s ./i18n/source\")\n\n\ti18nCmd.Flags().StringVarP(&i18nTargetPath, \"target\", \"t\", \"\", \"i18n target path, eg: -t ./i18n/target\")\n\n\tresetPasswordCmd.Flags().StringVarP(&resetPasswordEmail, \"email\", \"e\", \"\", \"user email address\")\n\tresetPasswordCmd.Flags().StringVarP(&resetPasswordPassword, \"password\", \"p\", \"\", \"new password (not recommended, will be recorded in shell history)\")\n\n\tfor _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd, resetPasswordCmd} {\n\t\trootCmd.AddCommand(cmd)\n\t}\n}\n\nvar (\n\trootCmd = &cobra.Command{\n\t\tUse:   \"answer\",\n\t\tShort: \"Answer is a minimalist open source Q&A community.\",\n\t\tLong: `Answer is a minimalist open source Q&A community.\nTo run answer, use:\n\t- 'answer init' to initialize the required environment.\n\t- 'answer run' to launch application.`,\n\t}\n\n\trunCmd = &cobra.Command{\n\t\tUse:   \"run\",\n\t\tShort: \"Run Answer\",\n\t\tLong:  `Start running Answer`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tpath.FormatAllPath(dataDirPath)\n\t\t\tfmt.Println(\"config file path: \", path.GetConfigFilePath())\n\t\t\tfmt.Println(\"Answer is starting..........................\")\n\t\t\trunApp()\n\t\t},\n\t}\n\n\tinitCmd = &cobra.Command{\n\t\tUse:   \"init\",\n\t\tShort: \"Initialize Answer\",\n\t\tLong:  `Initialize Answer with specified configuration`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\t// check config file and database. if config file exists and database is already created, init done\n\t\t\tcli.InstallAllInitialEnvironment(dataDirPath)\n\n\t\t\tconfigFileExist := cli.CheckConfigFile(path.GetConfigFilePath())\n\t\t\tif configFileExist {\n\t\t\t\tfmt.Println(\"config file exists, try to read the config...\")\n\t\t\t\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Println(\"read config failed: \", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(\"config file read successfully, try to connect database...\")\n\t\t\t\tif cli.CheckDBTableExist(c.Data.Database) {\n\t\t\t\t\tfmt.Println(\"connect to database successfully and table already exists, do nothing.\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// start installation server to install\n\t\t\tinstall.Run(path.GetConfigFilePath())\n\t\t},\n\t}\n\n\tupgradeCmd = &cobra.Command{\n\t\tUse:   \"upgrade\",\n\t\tShort: \"Upgrade Answer\",\n\t\tLong:  `Upgrade Answer to the latest version`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlog.SetLogger(log.NewStdLogger(os.Stdout))\n\t\t\tpath.FormatAllPath(dataDirPath)\n\t\t\tcli.InstallI18nBundle(true)\n\t\t\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"read config failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err = migrations.Migrate(c.Debug, c.Data.Database, c.Data.Cache, upgradeVersion); err != nil {\n\t\t\t\tfmt.Println(\"migrate failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Println(\"upgrade done\")\n\t\t},\n\t}\n\n\tdumpCmd = &cobra.Command{\n\t\tUse:   \"dump\",\n\t\tShort: \"Back up data\",\n\t\tLong:  `Back up database into an SQL file`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tfmt.Println(\"Answer is backing up data\")\n\t\t\tpath.FormatAllPath(dataDirPath)\n\t\t\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"read config failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = cli.DumpAllData(c.Data.Database, dumpDataPath)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"dump failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Println(\"Answer backed up the data successfully.\")\n\t\t},\n\t}\n\n\tcheckCmd = &cobra.Command{\n\t\tUse:   \"check\",\n\t\tShort: \"Check the required environment\",\n\t\tLong:  `Check if the current environment meets the startup requirements`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tpath.FormatAllPath(dataDirPath)\n\t\t\tfmt.Println(\"Start checking the required environment...\")\n\t\t\tif cli.CheckConfigFile(path.GetConfigFilePath()) {\n\t\t\t\tfmt.Println(\"config file exists [✔]\")\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"config file not exists [x]\")\n\t\t\t}\n\n\t\t\tif cli.CheckUploadDir() {\n\t\t\t\tfmt.Println(\"upload directory exists [✔]\")\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"upload directory not exists [x]\")\n\t\t\t}\n\n\t\t\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"read config failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif cli.CheckDBConnection(c.Data.Database) {\n\t\t\t\tfmt.Println(\"db connection successfully [✔]\")\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"db connection failed [x]\")\n\t\t\t}\n\t\t\tfmt.Println(\"check environment all done\")\n\t\t},\n\t}\n\n\tbuildCmd = &cobra.Command{\n\t\tUse:   \"build\",\n\t\tShort: \"Build Answer with plugins\",\n\t\tLong:  `Build a new Answer with plugins that you need`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tfmt.Printf(\"try to build a new answer with plugins:\\n%s\\n\", strings.Join(buildWithPlugins, \"\\n\"))\n\t\t\terr := cli.BuildNewAnswer(buildDir, buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{\n\t\t\t\tVersion:  Version,\n\t\t\t\tRevision: Revision,\n\t\t\t\tTime:     Time,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"build failed %v\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\tfmt.Printf(\"build new answer successfully %s\\n\", buildOutput)\n\t\t},\n\t}\n\n\tpluginCmd = &cobra.Command{\n\t\tUse:   \"plugin\",\n\t\tShort: \"Print all plugins packed in the binary\",\n\t\tLong:  `Print all plugins packed in the binary`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\t_ = plugin.CallBase(func(base plugin.Base) error {\n\t\t\t\tinfo := base.Info()\n\t\t\t\tfmt.Printf(\"%s[%s] made by %s\\n\", info.SlugName, info.Version, info.Author)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t},\n\t}\n\n\tconfigCmd = &cobra.Command{\n\t\tUse:   \"config\",\n\t\tShort: \"Set some config to default value\",\n\t\tLong:  `Set some config to default value`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tpath.FormatAllPath(dataDirPath)\n\n\t\t\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"read config failed: \", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfield := &cli.ConfigField{}\n\t\t\tfmt.Println(configFields)\n\t\t\tif len(configFields) > 0 {\n\t\t\t\tswitch configFields[0] {\n\t\t\t\tcase \"allow_password_login\":\n\t\t\t\t\tfield.AllowPasswordLogin = true\n\t\t\t\tcase \"deactivate_plugin\":\n\t\t\t\t\tif len(configFields) > 1 {\n\t\t\t\t\t\tfield.DeactivatePluginSlugName = configFields[1]\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tfmt.Printf(\"field %s not support\\n\", configFields[0])\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = cli.SetDefaultConfig(c.Data.Database, c.Data.Cache, field)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"set default config failed: \", err.Error())\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"set default config successfully\")\n\t\t\t}\n\t\t},\n\t}\n\n\ti18nCmd = &cobra.Command{\n\t\tUse:   \"i18n\",\n\t\tShort: \"Overwrite i18n files\",\n\t\tLong:  `Merge i18n files from plugins to original i18n files. It will overwrite the original i18n files`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tif err := cli.ReplaceI18nFilesLocal(i18nTargetPath); err != nil {\n\t\t\t\tfmt.Printf(\"replace i18n files failed %v\\n\", err)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"replace i18n files successfully\\n\")\n\t\t\t}\n\n\t\t\tfmt.Printf(\"try to merge i18n files from %q to %q\\n\", i18nSourcePath, i18nTargetPath)\n\n\t\t\tif err := cli.MergeI18nFilesLocal(i18nTargetPath, i18nSourcePath); err != nil {\n\t\t\t\tfmt.Printf(\"merge i18n files failed %v\\n\", err)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"merge i18n files successfully\\n\")\n\t\t\t}\n\t\t},\n\t}\n\n\tresetPasswordCmd = &cobra.Command{\n\t\tUse:     \"passwd\",\n\t\tAliases: []string{\"password\", \"reset-password\"},\n\t\tShort:   \"Reset user password\",\n\t\tLong:    \"Reset user password by email address.\",\n\t\tExample: `  # Interactive mode (recommended, safest)\n  answer passwd -C ./answer-data\n\n  # Specify email only (will prompt for password securely)\n  answer passwd -C ./answer-data --email user@example.com\n  answer passwd -C ./answer-data -e user@example.com\n\n  # Specify email and password (NOT recommended, will be recorded in shell history)\n  answer passwd -C ./answer-data -e user@example.com -p newpassword123`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\topts := &cli.ResetPasswordOptions{\n\t\t\t\tEmail:    resetPasswordEmail,\n\t\t\t\tPassword: resetPasswordPassword,\n\t\t\t}\n\t\t\tif err := cli.ResetPassword(context.Background(), dataDirPath, opts); err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t},\n\t}\n)\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "cmd/main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage answercmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/cron\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/joho/godotenv\"\n\t\"github.com/segmentfault/pacman\"\n\t\"github.com/segmentfault/pacman/contrib/log/zap\"\n\t\"github.com/segmentfault/pacman/contrib/server/http\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc init() {\n\t// Load .env if present, ignore error to keep backward compatibility\n\t_ = godotenv.Load()\n}\n\n// go build -ldflags \"-X github.com/apache/answer/cmd.Version=x.y.z\"\nvar (\n\t// Name is the name of the project\n\tName = \"answer\"\n\t// Version is the version of the project\n\tVersion = \"0.0.0\"\n\t// Revision is the git short commit revision number\n\t// If built without a Git repository, this field will be empty.\n\tRevision = \"\"\n\t// Time is the build time of the project\n\tTime = \"\"\n\t// GoVersion is the go version of the project\n\tGoVersion = \"1.23\"\n\t// log level\n\tlogLevel = os.Getenv(\"LOG_LEVEL\")\n\t// log path\n\tlogPath = os.Getenv(\"LOG_PATH\")\n)\n\n// Main\n// @securityDefinitions.apikey ApiKeyAuth\n// @in header\n// @name Authorization\nfunc Main() {\n\tlog.SetLogger(zap.NewLogger(\n\t\tlog.ParseLevel(logLevel), zap.WithName(\"answer\"), zap.WithPath(logPath)))\n\tExecute()\n}\n\nfunc runApp() {\n\tc, err := conf.ReadConfig(path.GetConfigFilePath())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tapp, cleanup, err := initApplication(\n\t\tc.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, c.UI, log.GetLogger())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tconstant.Version = Version\n\tconstant.Revision = Revision\n\tconstant.GoVersion = GoVersion\n\tschema.AppStartTime = time.Now()\n\tfmt.Println(\"answer Version:\", constant.Version, \" Revision:\", constant.Revision)\n\n\tdefer cleanup()\n\tif err := app.Run(context.Background()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {\n\tmanager.Run()\n\treturn pacman.NewApp(\n\t\tpacman.WithName(Name),\n\t\tpacman.WithVersion(Version),\n\t\tpacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),\n\t)\n}\n"
  },
  {
    "path": "cmd/wire.go",
    "content": "//go:build wireinject\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// The build tag makes sure the stub is not built in the final build.\n\npackage answercmd\n\nimport (\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/cron\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/server\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/controller\"\n\ttemplaterender \"github.com/apache/answer/internal/controller/template_render\"\n\t\"github.com/apache/answer/internal/controller_admin\"\n\t\"github.com/apache/answer/internal/repo\"\n\t\"github.com/apache/answer/internal/router\"\n\t\"github.com/apache/answer/internal/service\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/google/wire\"\n\t\"github.com/segmentfault/pacman\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// initApplication init application.\nfunc initApplication(\n\tdebug bool,\n\tserverConf *conf.Server,\n\tdbConf *data.Database,\n\tcacheConf *data.CacheConf,\n\ti18nConf *translator.I18n,\n\tswaggerConf *router.SwaggerConfig,\n\tserviceConf *service_config.ServiceConfig,\n\tuiConf *server.UI,\n\tlogConf log.Logger) (*pacman.Application, func(), error) {\n\tpanic(wire.Build(\n\t\tserver.ProviderSetServer,\n\t\trouter.ProviderSetRouter,\n\t\tcontroller.ProviderSetController,\n\t\tcontroller_admin.ProviderSetController,\n\t\ttemplaterender.ProviderSetTemplateRenderController,\n\t\tservice.ProviderSetService,\n\t\tcron.ProviderSetService,\n\t\trepo.ProviderSetRepo,\n\t\ttranslator.ProviderSet,\n\t\tmiddleware.ProviderSetMiddleware,\n\t\tnewApplication,\n\t))\n}\n"
  },
  {
    "path": "cmd/wire_gen.go",
    "content": "//go:build !wireinject\n// +build !wireinject\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Code generated by Wire. DO NOT EDIT.\n\n//go:generate go run github.com/google/wire/cmd/wire\n\npackage answercmd\n\nimport (\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/cron\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/server\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/controller\"\n\t\"github.com/apache/answer/internal/controller/template_render\"\n\t\"github.com/apache/answer/internal/controller_admin\"\n\t\"github.com/apache/answer/internal/repo/activity\"\n\t\"github.com/apache/answer/internal/repo/activity_common\"\n\t\"github.com/apache/answer/internal/repo/ai_conversation\"\n\t\"github.com/apache/answer/internal/repo/answer\"\n\t\"github.com/apache/answer/internal/repo/api_key\"\n\t\"github.com/apache/answer/internal/repo/auth\"\n\t\"github.com/apache/answer/internal/repo/badge\"\n\t\"github.com/apache/answer/internal/repo/badge_award\"\n\t\"github.com/apache/answer/internal/repo/badge_group\"\n\t\"github.com/apache/answer/internal/repo/captcha\"\n\t\"github.com/apache/answer/internal/repo/collection\"\n\t\"github.com/apache/answer/internal/repo/comment\"\n\t\"github.com/apache/answer/internal/repo/config\"\n\t\"github.com/apache/answer/internal/repo/export\"\n\t\"github.com/apache/answer/internal/repo/file_record\"\n\t\"github.com/apache/answer/internal/repo/limit\"\n\t\"github.com/apache/answer/internal/repo/meta\"\n\tnotification2 \"github.com/apache/answer/internal/repo/notification\"\n\t\"github.com/apache/answer/internal/repo/plugin_config\"\n\t\"github.com/apache/answer/internal/repo/question\"\n\t\"github.com/apache/answer/internal/repo/rank\"\n\t\"github.com/apache/answer/internal/repo/reason\"\n\t\"github.com/apache/answer/internal/repo/report\"\n\t\"github.com/apache/answer/internal/repo/review\"\n\t\"github.com/apache/answer/internal/repo/revision\"\n\t\"github.com/apache/answer/internal/repo/role\"\n\t\"github.com/apache/answer/internal/repo/search_common\"\n\t\"github.com/apache/answer/internal/repo/site_info\"\n\t\"github.com/apache/answer/internal/repo/tag\"\n\t\"github.com/apache/answer/internal/repo/tag_common\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\t\"github.com/apache/answer/internal/repo/user_external_login\"\n\t\"github.com/apache/answer/internal/repo/user_notification_config\"\n\t\"github.com/apache/answer/internal/router\"\n\t\"github.com/apache/answer/internal/service/action\"\n\tactivity2 \"github.com/apache/answer/internal/service/activity\"\n\tactivity_common2 \"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\tai_conversation2 \"github.com/apache/answer/internal/service/ai_conversation\"\n\t\"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/apikey\"\n\tauth2 \"github.com/apache/answer/internal/service/auth\"\n\tbadge2 \"github.com/apache/answer/internal/service/badge\"\n\tcollection2 \"github.com/apache/answer/internal/service/collection\"\n\t\"github.com/apache/answer/internal/service/collection_common\"\n\tcomment2 \"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\tconfig2 \"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/dashboard\"\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\texport2 \"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\tfile_record2 \"github.com/apache/answer/internal/service/file_record\"\n\t\"github.com/apache/answer/internal/service/follow\"\n\t\"github.com/apache/answer/internal/service/importer\"\n\tmeta2 \"github.com/apache/answer/internal/service/meta\"\n\t\"github.com/apache/answer/internal/service/meta_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/notification\"\n\t\"github.com/apache/answer/internal/service/notification_common\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\t\"github.com/apache/answer/internal/service/question_common\"\n\trank2 \"github.com/apache/answer/internal/service/rank\"\n\treason2 \"github.com/apache/answer/internal/service/reason\"\n\treport2 \"github.com/apache/answer/internal/service/report\"\n\t\"github.com/apache/answer/internal/service/report_handle\"\n\treview2 \"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\trole2 \"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/search_parser\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/siteinfo\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttag2 \"github.com/apache/answer/internal/service/tag\"\n\ttag_common2 \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/internal/service/uploader\"\n\t\"github.com/apache/answer/internal/service/user_admin\"\n\t\"github.com/apache/answer/internal/service/user_common\"\n\tuser_external_login2 \"github.com/apache/answer/internal/service/user_external_login\"\n\tuser_notification_config2 \"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/segmentfault/pacman\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// Injectors from wire.go:\n\n// initApplication init application.\nfunc initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, uiConf *server.UI, logConf log.Logger) (*pacman.Application, func(), error) {\n\tstaticRouter := router.NewStaticRouter(serviceConf)\n\ti18nTranslator, err := translator.NewTranslator(i18nConf)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tengine, err := data.NewDB(debug, dbConf)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcache, cleanup, err := data.NewCache(cacheConf)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdataData, cleanup2, err := data.NewData(engine, cache)\n\tif err != nil {\n\t\tcleanup()\n\t\treturn nil, nil, err\n\t}\n\tsiteInfoRepo := site_info.NewSiteInfo(dataData)\n\tsiteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)\n\tlangController := controller.NewLangController(i18nTranslator, siteInfoCommonService)\n\tauthRepo := auth.NewAuthRepo(dataData)\n\tapiKeyRepo := api_key.NewAPIKeyRepo(dataData)\n\tauthService := auth2.NewAuthService(authRepo, apiKeyRepo)\n\tuserRepo := user.NewUserRepo(dataData)\n\tuniqueIDRepo := unique.NewUniqueIDRepo(dataData)\n\tconfigRepo := config.NewConfigRepo(dataData)\n\tconfigService := config2.NewConfigService(configRepo)\n\tactivityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configService)\n\tuserRankRepo := rank.NewUserRankRepo(dataData, configService)\n\tuserActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configService)\n\temailRepo := export.NewEmailRepo(dataData)\n\temailService := export2.NewEmailService(configService, emailRepo, siteInfoCommonService)\n\tuserRoleRelRepo := role.NewUserRoleRelRepo(dataData)\n\troleRepo := role.NewRoleRepo(dataData)\n\troleService := role2.NewRoleService(roleRepo)\n\tuserRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService)\n\tuserCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService, siteInfoCommonService)\n\tuserExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData)\n\tuserNotificationConfigRepo := user_notification_config.NewUserNotificationConfigRepo(dataData)\n\tuserNotificationConfigService := user_notification_config2.NewUserNotificationConfigService(userRepo, userNotificationConfigRepo)\n\tuserExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo, userNotificationConfigService)\n\tquestionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)\n\tanswerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)\n\tvoteRepo := activity_common.NewVoteRepo(dataData, activityRepo)\n\tfollowRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)\n\ttagRelRepo := tag.NewTagRelRepo(dataData, uniqueIDRepo)\n\ttagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)\n\trevisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)\n\trevisionService := revision_common.NewRevisionService(revisionRepo, userRepo)\n\tservice := activityqueue.NewService()\n\ttagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, service)\n\tcollectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)\n\tcollectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)\n\tanswerCommon := answercommon.NewAnswerCommon(answerRepo)\n\tmetaRepo := meta.NewMetaRepo(dataData)\n\tmetaCommonService := metacommon.NewMetaCommonService(metaRepo)\n\tquestionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, service, revisionRepo, siteInfoCommonService, dataData)\n\teventqueueService := eventqueue.NewService()\n\tfileRecordRepo := file_record.NewFileRecordRepo(dataData)\n\tfileRecordService := file_record2.NewFileRecordService(fileRecordRepo, revisionRepo, serviceConf, siteInfoCommonService, userCommon)\n\tuserService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventqueueService, fileRecordService)\n\tcaptchaRepo := captcha.NewCaptchaRepo(dataData)\n\tcaptchaService := action.NewCaptchaService(captchaRepo)\n\tuserController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService)\n\tcommentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)\n\tcommentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)\n\tobjService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)\n\tnoticequeueService := noticequeue.NewService()\n\texternalService := noticequeue.NewExternalService()\n\treviewRepo := review.NewReviewRepo(dataData)\n\treviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalService, tagCommonService, questionCommon, noticequeueService, siteInfoCommonService, commentCommonRepo)\n\tcommentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, noticequeueService, externalService, service, eventqueueService, reviewService)\n\trolePowerRelRepo := role.NewRolePowerRelRepo(dataData)\n\trolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)\n\trankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)\n\tlimitRepo := limit.NewRateLimitRepo(dataData)\n\trateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo)\n\tcommentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware)\n\treportRepo := report.NewReportRepo(dataData, uniqueIDRepo)\n\ttagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, service)\n\tanswerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, noticequeueService)\n\tanswerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService)\n\texternalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalService, userExternalLoginRepo, siteInfoCommonService)\n\tquestionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, noticequeueService, externalService, service, siteInfoCommonService, externalNotificationService, reviewService, configService, eventqueueService, reviewRepo)\n\tanswerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, noticequeueService, externalService, service, reviewService, eventqueueService)\n\treportHandle := report_handle.NewReportHandle(questionService, answerService, commentService)\n\treportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventqueueService)\n\treportController := controller.NewReportController(reportService, rankService, captchaService)\n\tcontentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, noticequeueService)\n\tvoteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService, eventqueueService)\n\tvoteController := controller.NewVoteController(voteService, rankService, captchaService)\n\ttagController := controller.NewTagController(tagService, tagCommonService, rankService)\n\tfollowFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)\n\tfollowService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo)\n\tfollowController := controller.NewFollowController(followService)\n\tcollectionGroupRepo := collection.NewCollectionGroupRepo(dataData)\n\tcollectionService := collection2.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)\n\tcollectionController := controller.NewCollectionController(collectionService)\n\tquestionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware)\n\tanswerController := controller.NewAnswerController(answerService, rankService, captchaService, siteInfoCommonService, rateLimitMiddleware)\n\tsearchParser := search_parser.NewSearchParser(tagCommonService, userCommon)\n\tsearchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon, tagCommonService)\n\tsearchService := content.NewSearchService(searchParser, searchRepo)\n\tsearchController := controller.NewSearchController(searchService, captchaService)\n\treviewActivityRepo := activity.NewReviewActivityRepo(dataData, activityRepo, userRankRepo, configService)\n\tcontentRevisionService := content.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, noticequeueService, service, reportRepo, reviewService, reviewActivityRepo)\n\trevisionController := controller.NewRevisionController(contentRevisionService, rankService)\n\trankController := controller.NewRankController(rankService)\n\tuserAdminRepo := user.NewUserAdminRepo(dataData, authRepo)\n\tnotificationRepo := notification2.NewNotificationRepo(dataData)\n\tpluginUserConfigRepo := plugin_config.NewPluginUserConfigRepo(dataData)\n\tbadgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo)\n\tuserAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo, notificationRepo, pluginUserConfigRepo, badgeAwardRepo)\n\tuserAdminController := controller_admin.NewUserAdminController(userAdminService)\n\treasonRepo := reason.NewReasonRepo(configService)\n\treasonService := reason2.NewReasonService(reasonRepo)\n\treasonController := controller.NewReasonController(reasonService)\n\tthemeController := controller_admin.NewThemeController()\n\tsiteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon, fileRecordService)\n\tsiteInfoController := controller_admin.NewSiteInfoController(siteInfoService)\n\tcontrollerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService)\n\tnotificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, noticequeueService, userExternalLoginRepo, siteInfoCommonService)\n\tbadgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo)\n\tnotificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService, badgeRepo)\n\tnotificationController := controller.NewNotificationController(notificationService, rankService)\n\tdashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData)\n\tdashboardController := controller.NewDashboardController(dashboardService)\n\tuploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService, fileRecordService)\n\tuploadController := controller.NewUploadController(uploaderService)\n\tactivityActivityRepo := activity.NewActivityRepo(dataData, configService)\n\tactivityCommon := activity_common2.NewActivityCommon(activityRepo, service)\n\tcommentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)\n\tactivityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaCommonService, configService)\n\tactivityController := controller.NewActivityController(activityService)\n\troleController := controller_admin.NewRoleController(roleService)\n\tpluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)\n\timporterService := importer.NewImporterService(questionService, rankService, userCommon)\n\tpluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, pluginUserConfigRepo, configService, dataData, importerService)\n\tpluginController := controller_admin.NewPluginController(pluginCommonService)\n\tpermissionController := controller.NewPermissionController(rankService)\n\tuserPluginController := controller.NewUserPluginController(pluginCommonService)\n\treviewController := controller.NewReviewController(reviewService, rankService, captchaService)\n\tmetaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventqueueService)\n\tmetaController := controller.NewMetaController(metaService)\n\tbadgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo)\n\teventRuleRepo := badge.NewEventRuleRepo(dataData)\n\tbadgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService, noticequeueService)\n\tbadgeEventService := badge2.NewBadgeEventService(dataData, eventqueueService, badgeRepo, eventRuleRepo, badgeAwardService)\n\tbadgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService, siteInfoCommonService)\n\tbadgeController := controller.NewBadgeController(badgeService, badgeAwardService)\n\tcontroller_adminBadgeController := controller_admin.NewBadgeController(badgeService)\n\tapiKeyService := apikey.NewAPIKeyService(apiKeyRepo)\n\tadminAPIKeyController := controller_admin.NewAdminAPIKeyController(apiKeyService)\n\tfeatureToggleService := feature_toggle.NewFeatureToggleService(siteInfoRepo)\n\tmcpController := controller.NewMCPController(searchService, siteInfoCommonService, tagCommonService, questionCommon, commentRepo, userCommon, answerRepo, featureToggleService)\n\taiConversationRepo := ai_conversation.NewAIConversationRepo(dataData)\n\taiConversationService := ai_conversation2.NewAIConversationService(aiConversationRepo, userCommon)\n\taiController := controller.NewAIController(searchService, siteInfoCommonService, tagCommonService, questionCommon, commentRepo, userCommon, answerRepo, mcpController, aiConversationService, featureToggleService)\n\taiConversationController := controller.NewAIConversationController(aiConversationService, featureToggleService)\n\taiConversationAdminController := controller_admin.NewAIConversationAdminController(aiConversationService, featureToggleService)\n\tanswerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController, adminAPIKeyController, aiController, aiConversationController, aiConversationAdminController, mcpController)\n\tswaggerRouter := router.NewSwaggerRouter(swaggerConf)\n\tuiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService)\n\tauthUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)\n\tavatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService)\n\tshortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService)\n\ttemplateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo)\n\ttemplateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventqueueService, userService, questionService)\n\ttemplateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware)\n\tconnectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)\n\tuserCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)\n\tuserCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)\n\tcaptchaController := controller.NewCaptchaController()\n\tembedController := controller.NewEmbedController()\n\trenderController := controller.NewRenderController()\n\tsidebarController := controller.NewSidebarController()\n\tpluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController, sidebarController)\n\tginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter, uiConf)\n\tscheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService, fileRecordService, userAdminService, serviceConf)\n\tapplication := newApplication(serverConf, ginEngine, scheduledTaskManager)\n\treturn application, func() {\n\t\tcleanup2()\n\t\tcleanup()\n\t}, nil\n}\n"
  },
  {
    "path": "configs/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage configs\n\nimport _ \"embed\"\n\n//go:embed  config.yaml\nvar Config []byte\n\n//go:embed  path_ignore.yaml\nvar PathIgnore []byte\n\n//go:embed  reserved-usernames.json\nvar ReservedUsernames []byte\n"
  },
  {
    "path": "configs/config.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nserver:\n  http:\n    addr: 0.0.0.0:80\ndata:\n  database:\n    driver: \"sqlite3\"\n    connection: \"/data/sqlite3/answer.db\"\n  cache:\n    file_path: \"/data/cache/cache.db\"\ni18n:\n  bundle_dir: \"/data/i18n\"\nswaggerui:\n  show: true\n  protocol: http\n  host: 127.0.0.1\n  address: ':80'\nservice_config:\n  upload_path: \"/data/uploads\"\n  clean_up_uploads: true\n  clean_orphan_uploads_period_hours: 48\n  purge_deleted_files_period_days: 30\nui:\n  public_url: '/'\n  api_url: '/'\n  base_url: ''\n  api_base_url: ''\n\n"
  },
  {
    "path": "configs/path_ignore.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# url path reserves the keywords list\nquestions:\n  - ask\ntags:\n  - create\nusers:\n  - unsubscribe\n  - settings\n  - login\n  - register\n  - account-recovery\n  - change-email\n  - password-reset\n  - account-activation\n  - confirm-new-email\n  - account-suspended\n  - confirm-email\n  - auth-landing\n"
  },
  {
    "path": "configs/reserved-usernames.json",
    "content": "[\"0\",\"100\",\"101\",\"102\",\"1xx\",\"200\",\"201\",\"202\",\"203\",\"204\",\"205\",\"206\",\"207\",\"226\",\"2xx\",\"300\",\"301\",\"302\",\"303\",\"304\",\"305\",\"307\",\"308\",\"3xx\",\"400\",\"401\",\"402\",\"403\",\"404\",\"405\",\"406\",\"407\",\"408\",\"409\",\"410\",\"411\",\"412\",\"413\",\"414\",\"415\",\"416\",\"417\",\"418\",\"422\",\"423\",\"424\",\"426\",\"428\",\"429\",\"431\",\"451\",\"4xx\",\"500\",\"501\",\"502\",\"503\",\"504\",\"505\",\"506\",\"507\",\"511\",\"5xx\",\"7xx\",\"about\",\"abuse\",\"access\",\"account\",\"account-activation\",\"account-recovery\",\"account-suspended\",\"accounts\",\"activate\",\"activities\",\"activity\",\"ad\",\"add\",\"address\",\"adm\",\"admin\",\"administration\",\"administrator\",\"ads\",\"adult\",\"advertising\",\"affiliate\",\"affiliates\",\"ajax\",\"all\",\"alpha\",\"analysis\",\"analytics\",\"android\",\"anon\",\"anonymous\",\"api\",\"app\",\"apps\",\"archive\",\"archives\",\"article\",\"asct\",\"asset\",\"atom\",\"auth\",\"auth-landing\",\"authentication\",\"autoconfig\",\"avatar\",\"backup\",\"balancer-manager\",\"bank\",\"banner\",\"banners\",\"beta\",\"billing\",\"bin\",\"blog\",\"blogs\",\"board\",\"book\",\"bookmark\",\"bot\",\"bots\",\"broadcasthost\",\"bug\",\"bugs\",\"business\",\"cache\",\"cadastro\",\"calendar\",\"call\",\"campaign\",\"cancel\",\"captcha\",\"career\",\"careers\",\"cart\",\"categories\",\"category\",\"cgi\",\"cgi-bin\",\"changelog\",\"change-email\",\"chat\",\"check\",\"checking\",\"checkout\",\"client\",\"cliente\",\"clients\",\"code\",\"codereview\",\"comercial\",\"comment\",\"comments\",\"communities\",\"community\",\"company\",\"compare\",\"compras\",\"config\",\"configuration\",\"confirm-new-email\",\"confirm-email\",\"connect\",\"contact\",\"contact-us\",\"contact_us\",\"contactus\",\"contest\",\"contribute\",\"corp\",\"create\",\"crypt\",\"css\",\"dashboard\",\"data\",\"db\",\"default\",\"delete\",\"demo\",\"design\",\"designer\",\"destroy\",\"dev\",\"devel\",\"developer\",\"developers\",\"diagram\",\"diary\",\"dict\",\"dictionary\",\"die\",\"dir\",\"direct_messages\",\"directory\",\"dist\",\"dns\",\"doc\",\"docker\",\"docs\",\"documentation\",\"domain\",\"download\",\"downloads\",\"ecommerce\",\"edit\",\"editor\",\"edu\",\"education\",\"email\",\"employment\",\"empty\",\"end\",\"enterprise\",\"entries\",\"entry\",\"error\",\"errors\",\"eval\",\"event\",\"everyone\",\"exit\",\"explore\",\"export\",\"facebook\",\"faq\",\"favorite\",\"favorites\",\"fbi\",\"feature\",\"features\",\"feed\",\"feedback\",\"feeds\",\"file\",\"files\",\"firewall\",\"first\",\"flash\",\"fleet\",\"fleets\",\"flog\",\"follow\",\"followers\",\"following\",\"forgot\",\"forgot-password\",\"forgot_password\",\"forgotpassword\",\"form\",\"forum\",\"forums\",\"founder\",\"free\",\"friend\",\"friends\",\"ftp\",\"gadget\",\"gadgets\",\"game\",\"games\",\"get\",\"ghost\",\"gift\",\"gifts\",\"gist\",\"git\",\"github\",\"graph\",\"group\",\"groups\",\"guest\",\"guests\",\"help\",\"home\",\"homepage\",\"hooks\",\"host\",\"hosting\",\"hostmaster\",\"hostname\",\"howto\",\"hpg\",\"html\",\"http\",\"httpd\",\"https\",\"i\",\"iamges\",\"icon\",\"icons\",\"id\",\"idea\",\"ideas\",\"image\",\"images\",\"imap\",\"img\",\"index\",\"indice\",\"info\",\"information\",\"inquiry\",\"instagram\",\"intranet\",\"invitations\",\"invite\",\"ip\",\"ipad\",\"iphone\",\"irc\",\"is\",\"isatap\",\"issue\",\"issues\",\"it\",\"item\",\"items\",\"java\",\"javascript\",\"job\",\"jobs\",\"join\",\"js\",\"json\",\"jump\",\"keys\",\"keyserver\",\"knowledgebase\",\"language\",\"languages\",\"last\",\"ldap-status\",\"legal\",\"license\",\"link\",\"links\",\"linux\",\"list\",\"lists\",\"local\",\"localdomain\",\"localhost\",\"log\",\"log-in\",\"log-out\",\"log_in\",\"log_out\",\"login\",\"logout\",\"logs\",\"m\",\"mac\",\"mail\",\"mail1\",\"mail2\",\"mail3\",\"mail4\",\"mail5\",\"mailer\",\"mailer-daemon\",\"mailing\",\"maintenance\",\"manager\",\"manual\",\"map\",\"maps\",\"marketing\",\"master\",\"me\",\"media\",\"member\",\"members\",\"message\",\"messages\",\"messenger\",\"microblog\",\"microblogs\",\"mine\",\"mis\",\"mob\",\"mobile\",\"movie\",\"movies\",\"mp3\",\"msg\",\"msn\",\"music\",\"musicas\",\"mx\",\"my\",\"mysql\",\"name\",\"named\",\"names\",\"namespace\",\"namespaces\",\"nan\",\"navi\",\"navigation\",\"net\",\"network\",\"new\",\"news\",\"newsletter\",\"nick\",\"nickname\",\"no-reply\",\"nobody\",\"noc\",\"noreply\",\"notes\",\"noticias\",\"notification\",\"notifications\",\"notify\",\"ns\",\"ns1\",\"ns10\",\"ns2\",\"ns3\",\"ns4\",\"ns5\",\"ns6\",\"ns7\",\"ns8\",\"ns9\",\"null\",\"oauth\",\"oauth_clients\",\"offer\",\"offers\",\"official\",\"old\",\"online\",\"openid\",\"operator\",\"ops\",\"order\",\"orders\",\"organization\",\"organizations\",\"orgs\",\"overview\",\"owner\",\"owners\",\"package\",\"page\",\"pager\",\"pages\",\"panel\",\"passwd\",\"password\",\"password-reset\",\"patch\",\"payment\",\"perl\",\"phone\",\"photo\",\"photoalbum\",\"photos\",\"php\",\"phpmyadmin\",\"phppgadmin\",\"phpredisadmin\",\"pic\",\"pics\",\"ping\",\"plan\",\"plans\",\"plugin\",\"plugins\",\"policy\",\"pop\",\"pop3\",\"popular\",\"portal\",\"post\",\"postfix\",\"postmaster\",\"posts\",\"pr\",\"premium\",\"press\",\"price\",\"pricing\",\"privacy\",\"privacy-policy\",\"privacy_policy\",\"privacypolicy\",\"private\",\"product\",\"products\",\"profile\",\"project\",\"projects\",\"promo\",\"pub\",\"public\",\"purpose\",\"put\",\"pw\",\"python\",\"query\",\"random\",\"ranking\",\"read\",\"readme\",\"recent\",\"recruit\",\"recruitment\",\"register\",\"registration\",\"release\",\"releases\",\"remote\",\"remove\",\"replies\",\"reply\",\"report\",\"reports\",\"repositories\",\"repository\",\"req\",\"request\",\"requests\",\"res\",\"reset\",\"reset-password\",\"reset_password\",\"resetpassword\",\"resource\",\"resources\",\"roc\",\"root\",\"rss\",\"ruby\",\"rule\",\"rules\",\"sag\",\"sale\",\"sales\",\"sample\",\"samples\",\"save\",\"school\",\"script\",\"scripts\",\"search\",\"secure\",\"security\",\"self\",\"send\",\"server\",\"server-info\",\"server-status\",\"service\",\"services\",\"session\",\"sessions\",\"setting\",\"settings\",\"setup\",\"share\",\"shop\",\"show\",\"sign-in\",\"sign-up\",\"sign_in\",\"sign_up\",\"signin\",\"signout\",\"signup\",\"site\",\"sitemap\",\"sites\",\"smartphone\",\"smtp\",\"soporte\",\"source\",\"spec\",\"special\",\"sql\",\"src\",\"ssh\",\"ssl\",\"ssladmin\",\"ssladministrator\",\"sslwebmaster\",\"staff\",\"stage\",\"staging\",\"start\",\"stat\",\"state\",\"static\",\"stats\",\"status\",\"store\",\"stores\",\"stories\",\"style\",\"styleguide\",\"styles\",\"stylesheet\",\"stylesheets\",\"subdomain\",\"subscribe\",\"subscriptions\",\"suporte\",\"support\",\"svn\",\"swf\",\"sys\",\"sysadmin\",\"sysadministrator\",\"system\",\"tablet\",\"tablets\",\"tag\",\"tags\",\"talk\",\"task\",\"tasks\",\"team\",\"teams\",\"tech\",\"telnet\",\"term\",\"terms\",\"terms-of-service\",\"terms_of_service\",\"termsofservice\",\"test\",\"test1\",\"test2\",\"test3\",\"teste\",\"testing\",\"tests\",\"theme\",\"themes\",\"thread\",\"threads\",\"tls\",\"tmp\",\"todo\",\"token\",\"tokenserver\",\"tool\",\"tools\",\"top\",\"topic\",\"topics\",\"tos\",\"tour\",\"translations\",\"trends\",\"tutorial\",\"tux\",\"tv\",\"twitter\",\"undef\",\"unfollow\",\"unsubscribe\",\"update\",\"upload\",\"uploads\",\"uptime\",\"url\",\"usage\",\"usenet\",\"user\",\"username\",\"users\",\"usr\",\"usuario\",\"util\",\"uucp\",\"vendas\",\"ver\",\"version\",\"video\",\"videos\",\"visitor\",\"vpn\",\"watch\",\"weather\",\"web\",\"webhook\",\"webhooks\",\"webmail\",\"webmaster\",\"website\",\"websites\",\"welcome\",\"widget\",\"widgets\",\"wiki\",\"win\",\"windows\",\"word\",\"work\",\"works\",\"workshop\",\"wpad\",\"ww\",\"wws\",\"www\",\"www1\",\"www2\",\"www3\",\"www4\",\"www5\",\"www6\",\"www7\",\"wwws\",\"wwww\",\"xfn\",\"xml\",\"xmpp\",\"xpg\",\"xxx\",\"yaml\",\"year\",\"yml\",\"you\",\"yourdomain\",\"yourname\",\"yoursite\",\"yourusername\"]"
  },
  {
    "path": "crowdin.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nfiles:\n  - source: /i18n/en_US.yaml\n    translation: /i18n/%locale_with_underscore%.yaml\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nversion: \"3\"\nservices:\n  answer:\n    image: apache/answer\n    ports:\n      - '9080:80'\n    restart: on-failure\n    volumes:\n      - answer-data:/data\n\nvolumes:\n  answer-data:\n"
  },
  {
    "path": "docs/docs.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Package docs Code generated by swaggo/swag. DO NOT EDIT\npackage docs\n\nimport \"github.com/swaggo/swag\"\n\nconst docTemplate = `{\n    \"schemes\": {{ marshal .Schemes }},\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"{{escape .Description}}\",\n        \"title\": \"{{.Title}}\",\n        \"contact\": {},\n        \"version\": \"{{.Version}}\"\n    },\n    \"host\": \"{{.Host}}\",\n    \"basePath\": \"{{.BasePath}}\",\n    \"paths\": {\n        \"/\": {\n            \"get\": {\n                \"description\": \"if config file not exist try to redirect to install page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"if config file not exist try to redirect to install page\",\n                \"responses\": {}\n            }\n        },\n        \"/answer/admin/api/ai-config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteAIResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update AI configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update AI configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AI config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteAIReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai-models\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI models\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI models\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAIModelResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai-provider\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI provider configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI provider configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAIProviderResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai/conversation\": {\n            \"get\": {\n                \"description\": \"get conversation detail for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"get conversation detail for admin\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"conversation id\",\n                        \"name\": \"conversation_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AIConversationAdminDetailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"description\": \"delete conversation and its related records for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"delete conversation for admin\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AIConversationAdminDeleteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai/conversation/page\": {\n            \"get\": {\n                \"description\": \"get conversation list for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"get conversation list for admin\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.AIConversationAdminListItem\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/answer/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Status:[available,deleted,pending]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"AdminAnswerPage admin answer page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"available\",\n                            \"deleted\",\n                            \"pending\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"answer id or question title\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/answer/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update answer status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update answer status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AdminUpdateAnswerStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AdminUpdateAnswerStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/api-key\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AddAPIKeyResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"delete apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.DeleteAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/api-key/all\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all api keys\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get all api keys\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAPIKeyResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/badge/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update badge status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminBadge\"\n                ],\n                \"summary\": \"update badge status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdateBadgeStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateBadgeStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/badges\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list all badges by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminBadge\"\n                ],\n                \"summary\": \"list all badges by page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"\",\n                            \"active\",\n                            \"inactive\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"badge status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"search param\",\n                        \"name\": \"q\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetBadgeListPagedResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/dashboard\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"DashboardInfo\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"DashboardInfo\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/delete/permanently\": {\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete permanently\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"delete permanently\",\n                \"parameters\": [\n                    {\n                        \"description\": \"DeletePermanentlyReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.DeletePermanentlyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/language/options\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"Get language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/mcp-config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get MCP configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get MCP configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteMCPResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update MCP configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update MCP configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"MCP config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteMCPReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugin/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"get plugin config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"plugin_slug_name\",\n                        \"name\": \"plugin_slug_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPluginConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"update plugin config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePluginConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugin/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update plugin status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"update plugin status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePluginStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugins\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"get plugin list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"status: active/inactive\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"have config\",\n                        \"name\": \"have_config\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/question/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Status:[available,closed,deleted,pending]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"AdminQuestionPage admin question page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"available\",\n                            \"closed\",\n                            \"deleted\",\n                            \"pending\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question id or title\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/question/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update question status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AdminUpdateQuestionStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AdminUpdateQuestionStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/reasons\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reasons by object type and action\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"reason\"\n                ],\n                \"summary\": \"get reasons by object type and action\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"comment\",\n                            \"user\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object_type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"status\",\n                            \"close\",\n                            \"flag\",\n                            \"review\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/roles\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get role list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get role list\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetRoleResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/setting/privileges\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetPrivilegesConfig get privileges config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"GetPrivilegesConfig get privileges config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPrivilegesConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update privileges config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update privileges config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePrivilegesConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/setting/smtp\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetSMTPConfig get smtp config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"GetSMTPConfig get smtp config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetSMTPConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update smtp config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update smtp config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"smtp config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateSMTPConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/advanced\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site advanced setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site advanced setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteAdvancedResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site advanced info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site advanced info\",\n                \"parameters\": [\n                    {\n                        \"description\": \"advanced settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteAdvancedReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/branding\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteBrandingResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info branding\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info branding\",\n                \"parameters\": [\n                    {\n                        \"description\": \"branding info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteBrandingReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/custom-css-html\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info custom html css config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info custom html css config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site custom css html config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site custom css html config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/general\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site general information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site general information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteGeneralResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site general information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site general information\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteGeneralReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/interface\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteInterfaceSettingsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info interface\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteInterfaceReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/login\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info login config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info login config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site login\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteLoginReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/polices\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get the policies information for the site\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get the policies information for the site\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SitePoliciesResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site policies configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site policies configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"write info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SitePoliciesReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/question\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site questions setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site questions setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteQuestionsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site question settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site question settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"questions settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteQuestionsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/security\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get the security information for the site\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get the security information for the site\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteSecurityResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site security configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site security configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"write info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteSecurityReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/seo\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site seo information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site seo information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteSeoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site seo information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site seo information\",\n                \"parameters\": [\n                    {\n                        \"description\": \"seo\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteSeoReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/tag\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site tags setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site tags setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteTagsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site tag settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site tag settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tags settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteTagsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/theme\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info theme config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info theme config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteThemeResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site custom css html config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site custom css html config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteThemeReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/users\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site user config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site user config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteUsersResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info config about users\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info config about users\",\n                \"parameters\": [\n                    {\n                        \"description\": \"users info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteUsersReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/users-settings\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteUsersSettingsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info users settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info users settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteUsersSettingsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/theme/options\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get theme options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get theme options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddUserReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/activation\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user activation\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get user activation\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user id\",\n                        \"name\": \"user_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserActivationResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/password\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user password\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserPasswordReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/profile\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"edit user profile\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"edit user profile\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.EditUserProfileReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/role\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user role\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user role\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserRoleReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add users\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add users\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddUsersReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users/activation\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"send user activation\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"send user activation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"SendUserActivationReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SendUserActivationReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get user page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"search query: email, username or id:[id]\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"staff user\",\n                        \"name\": \"staff\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"suspended\",\n                            \"deleted\",\n                            \"inactive\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"records\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUserPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/activity/timeline\": {\n            \"get\": {\n                \"description\": \"get object timeline\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get object timeline\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag slug name\",\n                        \"name\": \"tag_slug_name\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"tag\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"is show vote\",\n                        \"name\": \"show_vote\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetObjectTimelineResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/activity/timeline/detail\": {\n            \"get\": {\n                \"description\": \"get object timeline detail\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get object timeline detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"revision id\",\n                        \"name\": \"revision_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetObjectTimelineResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation\": {\n            \"get\": {\n                \"description\": \"get conversation detail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"get conversation detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"conversation id\",\n                        \"name\": \"conversation_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AIConversationDetailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation/page\": {\n            \"get\": {\n                \"description\": \"get conversation list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"get conversation list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.AIConversationListItem\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation/vote\": {\n            \"post\": {\n                \"description\": \"vote record\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"vote record\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote request\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AIConversationVoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Update Answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Update Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AnswerUpdateReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AnswerUpdateReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Add Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"add answer request\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AnswerAddReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"delete answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"answer\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/acceptance\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Accept Answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Accept Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AcceptAnswerReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AcceptAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/info\": {\n            \"get\": {\n                \"description\": \"Get Answer Detail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Get Answer Detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetAnswerInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/page\": {\n            \"get\": {\n                \"description\": \"AnswerList \\u003cbr\\u003e \\u003cb\\u003eorder\\u003c/b\\u003e (default or updated)\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"AnswerList\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question_id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover the deleted answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"recover answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"answer\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RecoverAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge\": {\n            \"get\": {\n                \"description\": \"get badge info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get badge info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetBadgeInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/awards/page\": {\n            \"get\": {\n                \"description\": \"get badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"badge id\",\n                        \"name\": \"badge_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"only list the award by username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetBadgeInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/user/awards\": {\n            \"get\": {\n                \"description\": \"get user badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get user badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user name\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserBadgeAwardListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/user/awards/recent\": {\n            \"get\": {\n                \"description\": \"get user badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get user badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user name\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserBadgeAwardListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badges\": {\n            \"get\": {\n                \"description\": \"list all badges group by group\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"list all badges group by group\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetBadgeListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/collection/switch\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add collection\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Collection\"\n                ],\n                \"summary\": \"add collection\",\n                \"parameters\": [\n                    {\n                        \"description\": \"collection\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.CollectionSwitchReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.CollectionSwitchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/comment\": {\n            \"get\": {\n                \"description\": \"get comment by id\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get comment by id\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"update comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"add comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"remove comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"remove comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/comment/page\": {\n            \"get\": {\n                \"description\": \"get comment page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get comment page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"vote\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"query condition\",\n                        \"name\": \"query_cond\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/binding/email\": {\n            \"post\": {\n                \"description\": \"external login binding user send email\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"external login binding user send email\",\n                \"parameters\": [\n                    {\n                        \"description\": \"external login binding user send email\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ExternalLoginBindingUserSendEmailReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ExternalLoginBindingUserSendEmailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all enabled connectors\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"get all enabled connectors\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.ConnectorInfoResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all connectors info about user\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"get all connectors info about user\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.ConnectorUserInfoResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/user/unbinding\": {\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"unbind external user login\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"unbind external user login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"ExternalLoginUnbindingReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ExternalLoginUnbindingReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/embed/config\": {\n            \"get\": {\n                \"description\": \"get embed plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Plugin\"\n                ],\n                \"summary\": \"get embed plugin config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/plugin.EmbedConfig\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/file\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"upload file\",\n                \"consumes\": [\n                    \"multipart/form-data\"\n                ],\n                \"tags\": [\n                    \"Upload\"\n                ],\n                \"summary\": \"upload file\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"post\",\n                            \"post_attachment\",\n                            \"avatar\",\n                            \"branding\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"identify the source of the file upload\",\n                        \"name\": \"source\",\n                        \"in\": \"formData\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"file\",\n                        \"description\": \"file\",\n                        \"name\": \"file\",\n                        \"in\": \"formData\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/follow\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"follow object or cancel follow operation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"follow object or cancel follow operation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"follow\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.FollowReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.FollowResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/follow/tags\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user follow tags\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"update user follow tags\",\n                \"parameters\": [\n                    {\n                        \"description\": \"follow\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateFollowTagsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/language/config\": {\n            \"get\": {\n                \"description\": \"get language config mapping\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get language config mapping\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Accept-Language\",\n                        \"name\": \"Accept-Language\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/language/options\": {\n            \"get\": {\n                \"description\": \"Get language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"Get language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/meta/reaction\": {\n            \"get\": {\n                \"description\": \"get reaction for an object\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Meta\"\n                ],\n                \"summary\": \"get reaction\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object_id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ReactionRespItem\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update reaction. if not exist, add one\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Meta\"\n                ],\n                \"summary\": \"add or update reaction\",\n                \"parameters\": [\n                    {\n                        \"description\": \"reaction\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateReactionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get notification list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"get notification list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"inbox\",\n                            \"achievement\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"type\",\n                        \"name\": \"type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"all\",\n                            \"posts\",\n                            \"invites\",\n                            \"votes\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"inbox_type\",\n                        \"name\": \"inbox_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/read/state\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ClearUnRead\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"ClearUnRead\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearIDRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearIDRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/read/state/all\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ClearUnRead\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"ClearUnRead\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/status\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetRedDot\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"GetRedDot\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"DelRedDot\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"DelRedDot\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/permission\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"check user permission\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Permission\"\n                ],\n                \"summary\": \"check user permission\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"question.add\",\n                            \"question.edit\",\n                            \"question.edit_without_review\",\n                            \"question.delete\",\n                            \"question.close\",\n                            \"question.reopen\",\n                            \"question.vote_up\",\n                            \"question.vote_down\",\n                            \"question.pin\",\n                            \"question.unpin\",\n                            \"question.hide\",\n                            \"question.show\",\n                            \"answer.add\",\n                            \"answer.edit\",\n                            \"answer.edit_without_review\",\n                            \"answer.delete\",\n                            \"answer.accept\",\n                            \"answer.vote_up\",\n                            \"answer.vote_down\",\n                            \"answer.invite_someone_to_answer\",\n                            \"comment.add\",\n                            \"comment.edit\",\n                            \"comment.delete\",\n                            \"comment.vote_up\",\n                            \"comment.vote_down\",\n                            \"report.add\",\n                            \"tag.add\",\n                            \"tag.edit\",\n                            \"tag.edit_slug_name\",\n                            \"tag.edit_without_review\",\n                            \"tag.delete\",\n                            \"tag.synonym\",\n                            \"link.url_limit\",\n                            \"vote.detail\",\n                            \"answer.audit\",\n                            \"question.audit\",\n                            \"tag.audit\",\n                            \"tag.use_reserved_tag\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"permission key\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"type\": \"boolean\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/answer/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal answers\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Personal\"\n                ],\n                \"summary\": \"list personal answers\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"score\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/collection/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal collections\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Collection\"\n                ],\n                \"summary\": \"list personal collections\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/comment/page\": {\n            \"get\": {\n                \"description\": \"user personal comment list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"user personal comment list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentPersonalWithPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/qa/top\": {\n            \"get\": {\n                \"description\": \"UserTop\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"UserTop\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/rank/page\": {\n            \"get\": {\n                \"description\": \"user personal rank list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Rank\"\n                ],\n                \"summary\": \"user personal rank list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetRankPersonalPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetOtherUserInfoByUsername\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"GetOtherUserInfoByUsername\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetOtherUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/vote/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user personal votes\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"get user personal votes\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetVoteWithPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/plugin/status\": {\n            \"get\": {\n                \"description\": \"get all plugins status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Plugin\"\n                ],\n                \"summary\": \"get all plugins status\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/post/render\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"render post content\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Upload\"\n                ],\n                \"summary\": \"render post content\",\n                \"parameters\": [\n                    {\n                        \"description\": \"PostRenderReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.PostRenderReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"update question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionUpdate\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"add question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionAdd\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"delete question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/answer\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add question and answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"add question and answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionAddByAnswer\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/info\": {\n            \"get\": {\n                \"description\": \"get question details\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question details\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"1\",\n                        \"description\": \"Question TagID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/invite\": {\n            \"get\": {\n                \"description\": \"get question invite user info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question invite user info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"1\",\n                        \"description\": \"Question ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question invite user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"update question invite user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionUpdateInviteUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/link\": {\n            \"get\": {\n                \"description\": \"get question link\",\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question link\",\n                \"parameters\": [\n                    {\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"in_days\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"active\",\n                            \"hot\",\n                            \"score\",\n                            \"unanswered\",\n                            \"recommend\",\n                            \"frequent\"\n                        ],\n                        \"type\": \"string\",\n                        \"name\": \"order\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"maximum\": 100,\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/operation\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Operation question \\\\n operation [pin unpin hide show]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Operation question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.OperationQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/page\": {\n            \"get\": {\n                \"description\": \"get questions by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get questions by page\",\n                \"parameters\": [\n                    {\n                        \"description\": \"QuestionPageReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionPageReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/recommend/page\": {\n            \"get\": {\n                \"description\": \"get recommend questions by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get recommend questions by page\",\n                \"parameters\": [\n                    {\n                        \"description\": \"QuestionPageReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionPageReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover deleted question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"recover deleted question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionRecoverReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/reopen\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"reopen question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"reopen question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ReopenQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/similar\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"fuzzy query similar questions based on title\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"fuzzy query similar questions based on title\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"title\",\n                        \"name\": \"title\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/similar/tag\": {\n            \"get\": {\n                \"description\": \"Search Similar Question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Search Similar Question\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"question_id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Close question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Close question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.CloseQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/tags\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get tag list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag\",\n                        \"name\": \"tag\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetTagBasicResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/reasons\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reasons by object type and action\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"reason\"\n                ],\n                \"summary\": \"get reasons by object type and action\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"comment\",\n                            \"user\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object_type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"status\",\n                            \"close\",\n                            \"flag\",\n                            \"review\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/render/config\": {\n            \"get\": {\n                \"description\": \"GetRenderConfig\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginRender\"\n                ],\n                \"summary\": \"GetRenderConfig\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/plugin.RenderConfig\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add report \\u003cbr\\u003e source (question, answer, comment, user)\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"add report\",\n                \"parameters\": [\n                    {\n                        \"description\": \"report\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddReportReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report/review\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"review report\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"review report\",\n                \"parameters\": [\n                    {\n                        \"description\": \"flag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ReviewReportReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report/unreviewed/post\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed report post page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"get unreviewed report post page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetReportListPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/review/pending/post\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update review\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Review\"\n                ],\n                \"summary\": \"update review\",\n                \"parameters\": [\n                    {\n                        \"description\": \"review\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateReviewReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/review/pending/post/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed post page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Review\"\n                ],\n                \"summary\": \"get unreviewed post page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object_id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUnreviewedPostPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/reviewing/type\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reviewing type\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get reviewing type\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetReviewingTypeResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions\": {\n            \"get\": {\n                \"description\": \"get revision list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get revision list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetRevisionResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/audit\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"revision audit operation:approve or reject\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"revision audit\",\n                \"parameters\": [\n                    {\n                        \"description\": \"audit\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RevisionAuditReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/edit/check\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"check can update revision\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"check can update revision\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/unreviewed\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed revision list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get unreviewed revision list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page id\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUnreviewedRevisionResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/search\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"search object\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Search\"\n                ],\n                \"summary\": \"search object\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"query string\",\n                        \"name\": \"q\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"active\",\n                            \"score\",\n                            \"relevance\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SearchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/search/desc\": {\n            \"get\": {\n                \"description\": \"get search description\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Search\"\n                ],\n                \"summary\": \"get search description\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SearchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/siteinfo\": {\n            \"get\": {\n                \"description\": \"get site info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site info\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/siteinfo/legal\": {\n            \"get\": {\n                \"description\": \"get site legal info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site legal info\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"tos\",\n                            \"privacy\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"legal information type\",\n                        \"name\": \"info_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetSiteLegalInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag\": {\n            \"get\": {\n                \"description\": \"get tag one\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag one\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag id\",\n                        \"name\": \"tag_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag name\",\n                        \"name\": \"tag_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetTagResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"update tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"add tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"delete tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/merge\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"merge tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"merge tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover delete tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"recover delete tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RecoverTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/synonym\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"update tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateTagSynonymReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/synonyms\": {\n            \"get\": {\n                \"description\": \"get tag synonyms\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag synonyms\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"tag id\",\n                        \"name\": \"tag_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetTagSynonymsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags\": {\n            \"get\": {\n                \"description\": \"get tags list by slug name\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tags list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        },\n                        \"collectionFormat\": \"csv\",\n                        \"description\": \"string collection\",\n                        \"name\": \"tags\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetTagBasicResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags/following\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get following tag list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get following tag list\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetFollowingTagsResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags/page\": {\n            \"get\": {\n                \"description\": \"get tag page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"slug_name\",\n                        \"name\": \"slug_name\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"popular\",\n                            \"name\",\n                            \"newest\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"query condition\",\n                        \"name\": \"query_cond\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetTagPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/action/record\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ActionRecord\",\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"ActionRecord\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"login\",\n                            \"e_mail\",\n                            \"find_pass\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ActionRecordResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"user change email verification\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"user change email verification\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserChangeEmailVerifyReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserChangeEmailVerifyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/change/code\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"send email to the user email then change their email\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"send email to the user email then change their email\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserChangeEmailSendCodeReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserChangeEmailSendCodeReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/verification\": {\n            \"post\": {\n                \"description\": \"UserVerifyEmail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserVerifyEmail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"code\",\n                        \"name\": \"code\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/verification/send\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserVerifyEmailSend\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserVerifyEmailSend\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"captcha_id\",\n                        \"name\": \"captcha_id\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"captcha_code\",\n                        \"name\": \"captcha_code\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user info, if user no login response http code is 200, but user info is null\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"GetUserInfoByUserID\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetCurrentLoginUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserUpdateInfo update user info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserUpdateInfo update user info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"UpdateInfoRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateInfoRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/info/search\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"SearchUserListByName\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"SearchUserListByName\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetOtherUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/interface\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserUpdateInterface update user interface config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserUpdateInterface update user interface config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"UpdateInfoRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserInterfaceRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/login/email\": {\n            \"post\": {\n                \"description\": \"UserEmailLogin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserEmailLogin\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserEmailLogin\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserEmailLoginReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/logout\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"user logout\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"user logout\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/notification/config\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user's notification config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"update user's notification config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdateUserNotificationConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserNotificationConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user's notification config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user's notification config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserNotificationConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/notification/unsubscribe\": {\n            \"put\": {\n                \"description\": \"unsubscribe notification\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"unsubscribe notification\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserUnsubscribeNotificationReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserUnsubscribeNotificationReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserModifyPassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserModifyPassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserModifyPasswordReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserModifyPasswordReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password/replacement\": {\n            \"post\": {\n                \"description\": \"UseRePassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UseRePassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRePassWordRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRePassWordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password/reset\": {\n            \"post\": {\n                \"description\": \"RetrievePassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"RetrievePassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRetrievePassWordRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRetrievePassWordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/plugin/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user plugin config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"get user plugin config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"plugin_slug_name\",\n                        \"name\": \"plugin_slug_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPluginConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"update user plugin config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserPluginConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/plugin/configs\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin list that used for user.\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"get plugin list that used for user.\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/ranking\": {\n            \"get\": {\n                \"description\": \"get user ranking\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user ranking\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserRankingResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/register/email\": {\n            \"post\": {\n                \"description\": \"UserRegisterByEmail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserRegisterByEmail\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRegisterReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRegisterReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/staff\": {\n            \"get\": {\n                \"description\": \"get user staff\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user staff\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserStaffResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/vote/down\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add vote\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"vote down\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.VoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.VoteResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/vote/up\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add vote\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"vote up\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.VoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.VoteResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/custom.css\": {\n            \"get\": {\n                \"description\": \"get site custom CSS\",\n                \"produces\": [\n                    \"text/css\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site custom CSS\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/base-info\": {\n            \"post\": {\n                \"description\": \"init base info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"init base info\",\n                \"parameters\": [\n                    {\n                        \"description\": \"InitBaseInfoReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.InitBaseInfoReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/config-file/check\": {\n            \"post\": {\n                \"description\": \"check config file if exist when installation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"check config file if exist when installation\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/install.CheckConfigFileResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/db/check\": {\n            \"post\": {\n                \"description\": \"check database if exist when installation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"check database if exist when installation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"CheckDatabaseReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.CheckDatabaseReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/install.CheckConfigFileResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/init\": {\n            \"post\": {\n                \"description\": \"init environment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"init environment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"CheckDatabaseReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.CheckDatabaseReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/language/config\": {\n            \"get\": {\n                \"description\": \"get installation language config mapping\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get installation language config mapping\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"installation language\",\n                        \"name\": \"lang\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/language/options\": {\n            \"get\": {\n                \"description\": \"get installation language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get installation language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/translator.LangOption\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/personal/question/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal questions\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Personal\"\n                ],\n                \"summary\": \"list personal questions\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"score\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/robots.txt\": {\n            \"get\": {\n                \"description\": \"get site robots information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site robots information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"constant.NotificationChannelKey\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"email\"\n            ],\n            \"x-enum-varnames\": [\n                \"EmailChannel\"\n            ]\n        },\n        \"constant.Privilege\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                }\n            }\n        },\n        \"entity.BadgeLevel\": {\n            \"type\": \"integer\",\n            \"enum\": [\n                1,\n                2,\n                3\n            ],\n            \"x-enum-varnames\": [\n                \"BadgeLevelBronze\",\n                \"BadgeLevelSilver\",\n                \"BadgeLevelGold\"\n            ]\n        },\n        \"handler.RespBody\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"description\": \"http code\",\n                    \"type\": \"integer\"\n                },\n                \"data\": {\n                    \"description\": \"response data\"\n                },\n                \"msg\": {\n                    \"description\": \"response message\",\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"description\": \"reason key\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"install.CheckConfigFileResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config_file_exist\": {\n                    \"type\": \"boolean\"\n                },\n                \"db_connection_success\": {\n                    \"type\": \"boolean\"\n                },\n                \"db_table_exist\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"install.CheckDatabaseReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"db_type\"\n            ],\n            \"properties\": {\n                \"db_file\": {\n                    \"type\": \"string\"\n                },\n                \"db_host\": {\n                    \"type\": \"string\"\n                },\n                \"db_name\": {\n                    \"type\": \"string\"\n                },\n                \"db_password\": {\n                    \"type\": \"string\"\n                },\n                \"db_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"postgres\",\n                        \"sqlite3\",\n                        \"mysql\"\n                    ]\n                },\n                \"db_username\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_cert\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"ssl_key\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_mode\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_root_cert\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"install.InitBaseInfoReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"email\",\n                \"external_content_display\",\n                \"lang\",\n                \"name\",\n                \"password\",\n                \"site_name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"lang\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"site_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"pager.PageModel\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"type\": \"integer\"\n                },\n                \"list\": {}\n            }\n        },\n        \"plugin.EmbedConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"platform\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"plugin.RenderConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"select_theme\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminDeleteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"conversation_id\"\n            ],\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminDetailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"records\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.AIConversationRecord\"\n                    }\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.AIConversationUserInfo\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminListItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"helpful_count\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"unhelpful_count\": {\n                    \"type\": \"integer\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.AIConversationUserInfo\"\n                }\n            }\n        },\n        \"schema.AIConversationDetailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"records\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.AIConversationRecord\"\n                    }\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AIConversationListItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationRecord\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"chat_completion_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"helpful\": {\n                    \"type\": \"integer\"\n                },\n                \"role\": {\n                    \"type\": \"string\"\n                },\n                \"unhelpful\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AIConversationUserInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationVoteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"chat_completion_id\",\n                \"vote_type\"\n            ],\n            \"properties\": {\n                \"cancel\": {\n                    \"type\": \"boolean\"\n                },\n                \"chat_completion_id\": {\n                    \"type\": \"string\"\n                },\n                \"vote_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"helpful\",\n                        \"unhelpful\"\n                    ]\n                }\n            }\n        },\n        \"schema.AIPromptConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"en_us\": {\n                    \"type\": \"string\"\n                },\n                \"zh_cn\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AcceptAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                }\n            }\n        },\n        \"schema.ActObjectInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ActObjectTimeline\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"activity_id\": {\n                    \"type\": \"string\"\n                },\n                \"activity_type\": {\n                    \"type\": \"string\"\n                },\n                \"cancelled\": {\n                    \"type\": \"boolean\"\n                },\n                \"cancelled_at\": {\n                    \"type\": \"integer\"\n                },\n                \"comment\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"revision_id\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                }\n            }\n        },\n        \"schema.ActionRecordResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_img\": {\n                    \"type\": \"string\"\n                },\n                \"verify\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.AddAPIKeyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"description\",\n                \"scope\"\n            ],\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150\n                },\n                \"scope\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"read-only\",\n                        \"global\"\n                    ]\n                }\n            }\n        },\n        \"schema.AddAPIKeyResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_key\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AddCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\",\n                \"original_text\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"mention_username_list\": {\n                    \"description\": \"@ user id list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 600,\n                    \"minLength\": 2\n                },\n                \"reply_comment_id\": {\n                    \"description\": \"reply comment id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AddReportReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\",\n                \"report_type\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"report content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\",\n                    \"maxLength\": 20\n                },\n                \"report_type\": {\n                    \"description\": \"report type\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AddTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"original_text\",\n                \"slug_name\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                }\n            }\n        },\n        \"schema.AddUserReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"email\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.AddUsersReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"users\": {\n                    \"description\": \"users info line by line\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AdminUpdateAnswerStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"available\",\n                        \"deleted\"\n                    ]\n                }\n            }\n        },\n        \"schema.AdminUpdateQuestionStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"available\",\n                        \"closed\",\n                        \"deleted\"\n                    ]\n                }\n            }\n        },\n        \"schema.AnswerAddReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"content\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AnswerInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted\": {\n                    \"type\": \"integer\"\n                },\n                \"collected\": {\n                    \"type\": \"boolean\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"create_time\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"question_info\": {\n                    \"$ref\": \"#/definitions/schema.QuestionInfoResp\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"update_time\": {\n                    \"type\": \"integer\"\n                },\n                \"update_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AnswerUpdateReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"content\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"edit_summary\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AvatarInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 200\n                },\n                \"gravatar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 200\n                },\n                \"type\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.BadgeListInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"earned_count\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.BadgeStatus\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"active\",\n                \"inactive\"\n            ],\n            \"x-enum-varnames\": [\n                \"BadgeStatusActive\",\n                \"BadgeStatusInactive\"\n            ]\n        },\n        \"schema.CloseQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"close_msg\": {\n                    \"description\": \"close_type\",\n                    \"type\": \"string\"\n                },\n                \"close_type\": {\n                    \"description\": \"close_type\",\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.CollectionSwitchReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"group_id\",\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"bookmark\": {\n                    \"type\": \"boolean\"\n                },\n                \"group_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.CollectionSwitchResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object_collection_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.ConfigField\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ConfigFieldOption\"\n                    }\n                },\n                \"required\": {\n                    \"type\": \"boolean\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"ui_options\": {\n                    \"$ref\": \"#/definitions/schema.ConfigFieldUIOptions\"\n                },\n                \"value\": {}\n            }\n        },\n        \"schema.ConfigFieldOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConfigFieldUIOptions\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"action\": {\n                    \"$ref\": \"#/definitions/schema.UIOptionAction\"\n                },\n                \"class_name\": {\n                    \"type\": \"string\"\n                },\n                \"field_class_name\": {\n                    \"type\": \"string\"\n                },\n                \"input_type\": {\n                    \"type\": \"string\"\n                },\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"placeholder\": {\n                    \"type\": \"string\"\n                },\n                \"rows\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                },\n                \"variant\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConnectorInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConnectorUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"binding\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_id\": {\n                    \"type\": \"string\"\n                },\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.DeleteAPIKeyReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.DeletePermanentlyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"type\"\n            ],\n            \"properties\": {\n                \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"users\",\n                        \"questions\",\n                        \"answers\"\n                    ]\n                }\n            }\n        },\n        \"schema.EditUserProfileReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"email\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                }\n            }\n        },\n        \"schema.ExternalLoginBindingUserSendEmailReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"binding_key\",\n                \"email\"\n            ],\n            \"properties\": {\n                \"binding_key\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"must\": {\n                    \"description\": \"If must is true, whatever email if exists, try to bind user.\\nIf must is false, when email exist, will only be prompted with a warning.\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.ExternalLoginBindingUserSendEmailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"type\": \"string\"\n                },\n                \"email_exist_and_must_be_confirmed\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.ExternalLoginUnbindingReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_id\"\n            ],\n            \"properties\": {\n                \"external_id\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.FollowReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"is_cancel\": {\n                    \"description\": \"is cancel\",\n                    \"type\": \"boolean\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.FollowResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"follows\": {\n                    \"description\": \"the followers of object\",\n                    \"type\": \"integer\"\n                },\n                \"is_followed\": {\n                    \"description\": \"if user is followed object will be true,otherwise false\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.GetAIModelResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"object\": {\n                    \"type\": \"string\"\n                },\n                \"owned_by\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAIProviderResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"default_api_host\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAPIKeyResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_key\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"last_used_at\": {\n                    \"type\": \"integer\"\n                },\n                \"scope\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAnswerInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.AnswerInfo\"\n                },\n                \"question\": {\n                    \"$ref\": \"#/definitions/schema.QuestionInfoResp\"\n                }\n            }\n        },\n        \"schema.GetBadgeInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"badge description\",\n                    \"type\": \"string\"\n                },\n                \"earned_count\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"is_single\": {\n                    \"description\": \"badge is single or multiple\",\n                    \"type\": \"boolean\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetBadgeListPagedResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"badge description\",\n                    \"type\": \"string\"\n                },\n                \"earned\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"boolean\"\n                },\n                \"group_name\": {\n                    \"description\": \"badge group name\",\n                    \"type\": \"string\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"badge status\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.BadgeStatus\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.GetBadgeListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"badges\": {\n                    \"description\": \"badge list info\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.BadgeListInfo\"\n                    }\n                },\n                \"group_name\": {\n                    \"description\": \"badge group name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetCommentPersonalWithPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetCommentResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"is_vote\": {\n                    \"description\": \"current user if already vote this comment\",\n                    \"type\": \"boolean\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"description\": \"parsed comment content\",\n                    \"type\": \"string\"\n                },\n                \"reply_comment_id\": {\n                    \"description\": \"reply comment id\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_display_name\": {\n                    \"description\": \"reply user display name\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_id\": {\n                    \"description\": \"reply user id\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_status\": {\n                    \"description\": \"reply user status\",\n                    \"type\": \"string\"\n                },\n                \"reply_username\": {\n                    \"description\": \"reply user username\",\n                    \"type\": \"string\"\n                },\n                \"user_avatar\": {\n                    \"description\": \"user avatar\",\n                    \"type\": \"string\"\n                },\n                \"user_display_name\": {\n                    \"description\": \"user display name\",\n                    \"type\": \"string\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"user_status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"vote_count\": {\n                    \"description\": \"user vote amount\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetCurrentLoginUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"description\": \"access token\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"authority_group\": {\n                    \"description\": \"authority group\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"$ref\": \"#/definitions/schema.AvatarInfo\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follow count\",\n                    \"type\": \"integer\"\n                },\n                \"have_password\": {\n                    \"description\": \"user have password\",\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mail_status\": {\n                    \"description\": \"mail status(1 pass 2 to be verified)\",\n                    \"type\": \"integer\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"notice_status\": {\n                    \"description\": \"notice status(1 on 2off)\",\n                    \"type\": \"integer\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"visit_token\": {\n                    \"description\": \"visit token\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetFollowingTagsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetObjectTimelineResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object_info\": {\n                    \"$ref\": \"#/definitions/schema.ActObjectInfo\"\n                },\n                \"timeline\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ActObjectTimeline\"\n                    }\n                }\n            }\n        },\n        \"schema.GetOtherUserInfoByUsernameResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"email\\nfollow count\",\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"status_msg\": {\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetOtherUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.GetOtherUserInfoByUsernameResp\"\n                }\n            }\n        },\n        \"schema.GetPluginConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ConfigField\"\n                    }\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetPluginListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"have_config\": {\n                    \"type\": \"boolean\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetPrivilegesConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PrivilegeOption\"\n                    }\n                },\n                \"selected_level\": {\n                    \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                }\n            }\n        },\n        \"schema.GetRankPersonalPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"rank_type\": {\n                    \"description\": \"rank type\",\n                    \"type\": \"string\"\n                },\n                \"reputation\": {\n                    \"description\": \"reputation\",\n                    \"type\": \"integer\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetReportListPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"author_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"flag_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"comment\"\n                    ]\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"$ref\": \"#/definitions/schema.ReasonItem\"\n                },\n                \"reason_content\": {\n                    \"type\": \"string\"\n                },\n                \"submit_at\": {\n                    \"type\": \"integer\"\n                },\n                \"submitter_user\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetReviewingTypeResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"todo_amount\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetRevisionResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content\": {},\n                \"create_at\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"use_id\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                }\n            }\n        },\n        \"schema.GetRoleResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetSMTPConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"encryption\": {\n                    \"description\": \"\\\"\\\" SSL TLS\",\n                    \"type\": \"string\"\n                },\n                \"from_email\": {\n                    \"type\": \"string\"\n                },\n                \"from_name\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_authentication\": {\n                    \"type\": \"boolean\"\n                },\n                \"smtp_host\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_password\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_port\": {\n                    \"type\": \"integer\"\n                },\n                \"smtp_username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetSiteLegalInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetTagBasicResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetTagPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"description\": \"created time\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"description\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\"\n                },\n                \"excerpt\": {\n                    \"description\": \"excerpt\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follower amount\",\n                    \"type\": \"integer\"\n                },\n                \"is_follower\": {\n                    \"description\": \"is follower\",\n                    \"type\": \"boolean\"\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"description\": \"parsed_text\",\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"description\": \"question amount\",\n                    \"type\": \"integer\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"description\": \"updated time\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetTagResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"excerpt\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"is_follower\": {\n                    \"type\": \"boolean\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"member_actions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"type\": \"integer\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetTagSynonymsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"synonyms\": {\n                    \"description\": \"synonyms\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagSynonym\"\n                    }\n                }\n            }\n        },\n        \"schema.GetUnreviewedPostPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"author_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"comment\"\n                    ]\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"type\": \"string\"\n                },\n                \"review_id\": {\n                    \"type\": \"integer\"\n                },\n                \"submit_at\": {\n                    \"type\": \"integer\"\n                },\n                \"submitter_display_name\": {\n                    \"type\": \"string\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUnreviewedRevisionResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.UnreviewedRevisionInfoInfo\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"unreviewed_info\": {\n                    \"$ref\": \"#/definitions/schema.GetRevisionResp\"\n                }\n            }\n        },\n        \"schema.GetUserActivationResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"activation_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserBadgeAwardListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"earned_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserNotificationConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"all_new_question\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"all_new_question_for_following_tags\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"inbox\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                }\n            }\n        },\n        \"schema.GetUserPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"deleted_at\": {\n                    \"description\": \"delete time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"role_name\": {\n                    \"description\": \"role name\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"user status(normal,suspended,deleted,inactive)\",\n                    \"type\": \"string\"\n                },\n                \"suspended_at\": {\n                    \"description\": \"suspended time\",\n                    \"type\": \"integer\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until time\",\n                    \"type\": \"integer\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserPluginListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserStaffResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetVoteWithPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                },\n                \"vote_type\": {\n                    \"description\": \"vote type\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.LoadingAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"state\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.NotificationChannelConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"key\": {\n                    \"$ref\": \"#/definitions/constant.NotificationChannelKey\"\n                }\n            }\n        },\n        \"schema.NotificationClearIDRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.NotificationClearRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"type\"\n            ],\n            \"properties\": {\n                \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"inbox\",\n                        \"achievement\"\n                    ]\n                }\n            }\n        },\n        \"schema.OnCompleteAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"refresh_form_config\": {\n                    \"type\": \"boolean\"\n                },\n                \"toast_return_message\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.Operation\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"$ref\": \"#/definitions/schema.OperationLevel\"\n                },\n                \"msg\": {\n                    \"type\": \"string\"\n                },\n                \"time\": {\n                    \"type\": \"integer\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.OperationLevel\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"info\",\n                \"danger\",\n                \"warning\",\n                \"secondary\"\n            ],\n            \"x-enum-varnames\": [\n                \"OperationLevelInfo\",\n                \"OperationLevelDanger\",\n                \"OperationLevelWarning\",\n                \"OperationLevelSecondary\"\n            ]\n        },\n        \"schema.OperationQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"operation\": {\n                    \"description\": \"operation [pin unpin hide show]\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PermissionMemberAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"action\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PostRenderReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PrivilegeLevel\": {\n            \"type\": \"integer\",\n            \"enum\": [\n                1,\n                2,\n                3,\n                99\n            ],\n            \"x-enum-varnames\": [\n                \"PrivilegeLevel1\",\n                \"PrivilegeLevel2\",\n                \"PrivilegeLevel3\",\n                \"PrivilegeLevelCustom\"\n            ]\n        },\n        \"schema.PrivilegeOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"level\": {\n                    \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                },\n                \"level_desc\": {\n                    \"type\": \"string\"\n                },\n                \"privileges\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/constant.Privilege\"\n                    }\n                }\n            }\n        },\n        \"schema.QuestionAdd\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"title\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionAddByAnswer\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_content\",\n                \"title\"\n            ],\n            \"properties\": {\n                \"answer_content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"mention_username_list\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answered\": {\n                    \"type\": \"boolean\"\n                },\n                \"collected\": {\n                    \"type\": \"boolean\"\n                },\n                \"collection_count\": {\n                    \"type\": \"integer\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"create_time\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"edit_time\": {\n                    \"type\": \"integer\"\n                },\n                \"extends_actions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"first_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"is_followed\": {\n                    \"type\": \"boolean\"\n                },\n                \"last_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"last_answered_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"operation\": {\n                    \"$ref\": \"#/definitions/schema.Operation\"\n                },\n                \"pin\": {\n                    \"type\": \"integer\"\n                },\n                \"show\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"unique_view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"update_time\": {\n                    \"type\": \"integer\"\n                },\n                \"update_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionPageReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"in_days\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"order\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"newest\",\n                        \"active\",\n                        \"hot\",\n                        \"score\",\n                        \"unanswered\",\n                        \"recommend\",\n                        \"frequent\"\n                    ]\n                },\n                \"page\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"page_size\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"tag\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.QuestionPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted_answer_id\": {\n                    \"description\": \"answer information\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"collection_count\": {\n                    \"type\": \"integer\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"last_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"operated_at\": {\n                    \"description\": \"operator information\",\n                    \"type\": \"integer\"\n                },\n                \"operation_type\": {\n                    \"type\": \"string\"\n                },\n                \"operator\": {\n                    \"$ref\": \"#/definitions/schema.QuestionPageRespOperator\"\n                },\n                \"pin\": {\n                    \"description\": \"1: unpin, 2: pin\",\n                    \"type\": \"integer\"\n                },\n                \"show\": {\n                    \"description\": \"0: show, 1: hide\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"unique_view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"view_count\": {\n                    \"description\": \"question statistical information\",\n                    \"type\": \"integer\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.QuestionPageRespOperator\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionRecoverReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\"\n            ],\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionUpdate\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"title\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"edit_summary\": {\n                    \"description\": \"edit summary\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"invite_user\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionUpdateInviteUser\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"invite_user\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                }\n            }\n        },\n        \"schema.ReactionRespItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"description\": \"Count is the number of users who reacted\",\n                    \"type\": \"integer\"\n                },\n                \"emoji\": {\n                    \"description\": \"Emoji is the reaction emoji\",\n                    \"type\": \"string\"\n                },\n                \"is_active\": {\n                    \"description\": \"IsActive is if current user has reacted\",\n                    \"type\": \"boolean\"\n                },\n                \"tooltip\": {\n                    \"description\": \"Tooltip is the user's name who reacted\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReasonItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content_type\": {\n                    \"type\": \"string\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"placeholder\": {\n                    \"type\": \"string\"\n                },\n                \"reason_key\": {\n                    \"type\": \"string\"\n                },\n                \"reason_type\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.RecoverAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_id\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RecoverTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"tag_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"comment_id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReopenQuestionReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReviewReportReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"flag_id\",\n                \"operation_type\"\n            ],\n            \"properties\": {\n                \"close_msg\": {\n                    \"type\": \"string\"\n                },\n                \"close_type\": {\n                    \"type\": \"integer\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"flag_id\": {\n                    \"type\": \"string\"\n                },\n                \"operation_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"edit_post\",\n                        \"close_post\",\n                        \"delete_post\",\n                        \"unlist_post\",\n                        \"ignore_report\"\n                    ]\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.RevisionAuditReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"operation\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"operation\": {\n                    \"description\": \"approve or reject\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SearchObject\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"excerpt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"Status\",\n                    \"type\": \"string\"\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"description\": \"user info\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.SearchObjectUser\"\n                        }\n                    ]\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SearchObjectUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SearchResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"type\": \"integer\"\n                },\n                \"list\": {\n                    \"description\": \"search response\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SearchResult\"\n                    }\n                }\n            }\n        },\n        \"schema.SearchResult\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object\": {\n                    \"description\": \"this object\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.SearchObject\"\n                        }\n                    ]\n                },\n                \"object_type\": {\n                    \"description\": \"object_type\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SendUserActivationReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteAIProvider\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"api_host\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"api_key\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"model\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                }\n            }\n        },\n        \"schema.SiteAIReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_providers\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteAIProvider\"\n                    }\n                },\n                \"chosen_provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"prompt_config\": {\n                    \"$ref\": \"#/definitions/schema.AIPromptConfig\"\n                }\n            }\n        },\n        \"schema.SiteAIResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_providers\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteAIProvider\"\n                    }\n                },\n                \"chosen_provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"prompt_config\": {\n                    \"$ref\": \"#/definitions/schema.AIPromptConfig\"\n                }\n            }\n        },\n        \"schema.SiteAdvancedReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"authorized_attachment_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"authorized_image_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"max_attachment_size\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_megapixel\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_size\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SiteAdvancedResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"authorized_attachment_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"authorized_image_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"max_attachment_size\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_megapixel\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_size\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SiteBrandingReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"favicon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"mobile_logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"square_icon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteBrandingResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"favicon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"mobile_logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"square_icon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteCustomCssHTMLReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom_css\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_footer\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_head\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_header\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_sidebar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                }\n            }\n        },\n        \"schema.SiteCustomCssHTMLResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom_css\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_footer\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_head\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_header\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_sidebar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                }\n            }\n        },\n        \"schema.SiteGeneralReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 2000\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"short_description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteGeneralResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 2000\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"short_description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"branding\": {\n                    \"$ref\": \"#/definitions/schema.SiteBrandingResp\"\n                },\n                \"custom_css_html\": {\n                    \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLResp\"\n                },\n                \"general\": {\n                    \"$ref\": \"#/definitions/schema.SiteGeneralResp\"\n                },\n                \"interface\": {\n                    \"$ref\": \"#/definitions/schema.SiteInterfaceSettingsResp\"\n                },\n                \"login\": {\n                    \"$ref\": \"#/definitions/schema.SiteLoginResp\"\n                },\n                \"mcp_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"revision\": {\n                    \"type\": \"string\"\n                },\n                \"site_advanced\": {\n                    \"$ref\": \"#/definitions/schema.SiteAdvancedResp\"\n                },\n                \"site_legal\": {\n                    \"$ref\": \"#/definitions/schema.SiteLegalSimpleResp\"\n                },\n                \"site_questions\": {\n                    \"$ref\": \"#/definitions/schema.SiteQuestionsResp\"\n                },\n                \"site_security\": {\n                    \"$ref\": \"#/definitions/schema.SiteSecurityResp\"\n                },\n                \"site_seo\": {\n                    \"$ref\": \"#/definitions/schema.SiteSeoResp\"\n                },\n                \"site_tags\": {\n                    \"$ref\": \"#/definitions/schema.SiteTagsResp\"\n                },\n                \"site_users\": {\n                    \"$ref\": \"#/definitions/schema.SiteUsersResp\"\n                },\n                \"theme\": {\n                    \"$ref\": \"#/definitions/schema.SiteThemeResp\"\n                },\n                \"users_settings\": {\n                    \"$ref\": \"#/definitions/schema.SiteUsersSettingsResp\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteInterfaceReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"language\",\n                \"time_zone\"\n            ],\n            \"properties\": {\n                \"language\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"time_zone\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.SiteInterfaceSettingsResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"language\",\n                \"time_zone\"\n            ],\n            \"properties\": {\n                \"language\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"time_zone\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.SiteLegalSimpleResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                }\n            }\n        },\n        \"schema.SiteLoginReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"allow_email_domains\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"allow_email_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_new_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_password_login\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteLoginResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"allow_email_domains\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"allow_email_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_new_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_password_login\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteMCPReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteMCPResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"http_header\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SitePoliciesReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SitePoliciesResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteQuestionsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"min_content\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 0\n                },\n                \"min_tags\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 5,\n                    \"minimum\": 0\n                },\n                \"restrict_answer\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteQuestionsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"min_content\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 0\n                },\n                \"min_tags\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 5,\n                    \"minimum\": 0\n                },\n                \"restrict_answer\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSecurityReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"check_update\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSecurityResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"check_update\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSeoReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"permalink\",\n                \"robots\"\n            ],\n            \"properties\": {\n                \"permalink\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 4,\n                    \"minimum\": 0\n                },\n                \"robots\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteSeoResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"permalink\",\n                \"robots\"\n            ],\n            \"properties\": {\n                \"permalink\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 4,\n                    \"minimum\": 0\n                },\n                \"robots\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteTagsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"recommend_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                },\n                \"required_tag\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteTagsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"recommend_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                },\n                \"required_tag\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteThemeReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"theme\"\n            ],\n            \"properties\": {\n                \"color_scheme\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"layout\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Full-width\",\n                        \"Fixed-width\"\n                    ]\n                },\n                \"theme\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"theme_config\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                }\n            }\n        },\n        \"schema.SiteThemeResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color_scheme\": {\n                    \"type\": \"string\"\n                },\n                \"layout\": {\n                    \"type\": \"string\"\n                },\n                \"theme\": {\n                    \"type\": \"string\"\n                },\n                \"theme_config\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"theme_options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ThemeOption\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteUsersReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"allow_update_avatar\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_bio\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_display_name\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_location\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_username\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_website\": {\n                    \"type\": \"boolean\"\n                },\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"allow_update_avatar\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_bio\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_display_name\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_location\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_username\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_website\": {\n                    \"type\": \"boolean\"\n                },\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersSettingsReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersSettingsResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteWriteTag\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"slug_name\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.TagItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                }\n            }\n        },\n        \"schema.TagResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.TagSynonym\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ThemeOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UIOptionAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"loading\": {\n                    \"$ref\": \"#/definitions/schema.LoadingAction\"\n                },\n                \"method\": {\n                    \"type\": \"string\"\n                },\n                \"on_complete\": {\n                    \"$ref\": \"#/definitions/schema.OnCompleteAction\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UnreviewedRevisionInfoInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"object_creator_user_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateAPIKeyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"description\",\n                \"id\"\n            ],\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.UpdateBadgeStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"badge status\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.BadgeStatus\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"comment_id\",\n                \"original_text\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"whether user can delete it\",\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 600,\n                    \"minLength\": 2\n                }\n            }\n        },\n        \"schema.UpdateFollowTagsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"slug_name_list\": {\n                    \"description\": \"tag slug name list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                }\n            }\n        },\n        \"schema.UpdateInfoRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"$ref\": \"#/definitions/schema.AvatarInfo\"\n                },\n                \"bio\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 4096\n                },\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"location\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"website\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UpdatePluginConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdatePluginStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdatePrivilegesConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"level\"\n            ],\n            \"properties\": {\n                \"custom_privileges\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/constant.Privilege\"\n                    }\n                },\n                \"level\": {\n                    \"minimum\": 1,\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateReactionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"emoji\",\n                \"object_id\",\n                \"reaction\"\n            ],\n            \"properties\": {\n                \"emoji\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"heart\",\n                        \"smile\",\n                        \"frown\"\n                    ]\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"reaction\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"activate\",\n                        \"deactivate\"\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateReviewReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"review_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"review_id\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"approve\",\n                        \"reject\"\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateSMTPConfigReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"encryption\": {\n                    \"description\": \"\\\"\\\" SSL TLS\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SSL\",\n                        \"TLS\"\n                    ]\n                },\n                \"from_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"from_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_authentication\": {\n                    \"type\": \"boolean\"\n                },\n                \"smtp_host\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_port\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 1\n                },\n                \"smtp_username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"test_email_recipient\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"edit_summary\": {\n                    \"description\": \"edit summary\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateTagSynonymReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"synonym_tag_list\",\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"synonym_tag_list\": {\n                    \"description\": \"synonym tag list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserInterfaceRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"color_scheme\",\n                \"language\"\n            ],\n            \"properties\": {\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdateUserNotificationConfigReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"all_new_question\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"all_new_question_for_following_tags\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"inbox\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                }\n            }\n        },\n        \"schema.UpdateUserPasswordReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"password\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserPluginConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdateUserRoleReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"role_id\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"status\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"remove_all_content\": {\n                    \"type\": \"boolean\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"normal\",\n                        \"suspended\",\n                        \"deleted\",\n                        \"inactive\"\n                    ]\n                },\n                \"suspend_duration\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"24h\",\n                        \"48h\",\n                        \"72h\",\n                        \"7d\",\n                        \"14d\",\n                        \"1m\",\n                        \"2m\",\n                        \"3m\",\n                        \"6m\",\n                        \"1y\",\n                        \"forever\"\n                    ]\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserBasicInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"type\": \"string\"\n                },\n                \"location\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserChangeEmailSendCodeReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserChangeEmailVerifyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UserEmailLoginReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserLoginResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"description\": \"access token\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"authority_group\": {\n                    \"description\": \"authority group\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follow count\",\n                    \"type\": \"integer\"\n                },\n                \"have_password\": {\n                    \"description\": \"user have password\",\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mail_status\": {\n                    \"description\": \"mail status(1 pass 2 to be verified)\",\n                    \"type\": \"integer\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"notice_status\": {\n                    \"description\": \"notice status(1 on 2off)\",\n                    \"type\": \"integer\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"visit_token\": {\n                    \"description\": \"visit token\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserModifyPasswordReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"old_pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserRankingResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"staffs\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                },\n                \"users_with_the_most_reputation\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                },\n                \"users_with_the_most_vote\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                }\n            }\n        },\n        \"schema.UserRankingSimpleInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"vote_count\": {\n                    \"description\": \"vote\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.UserRePassWordRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32\n                }\n            }\n        },\n        \"schema.UserRegisterReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\",\n                \"name\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserRetrievePassWordRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UserUnsubscribeNotificationReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.VoteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"is_cancel\": {\n                    \"type\": \"boolean\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.VoteResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"down_votes\": {\n                    \"type\": \"integer\"\n                },\n                \"up_votes\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                },\n                \"votes\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"translator.LangOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"progress\": {\n                    \"description\": \"Translation completion percentage\",\n                    \"type\": \"integer\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        }\n    },\n    \"securityDefinitions\": {\n        \"ApiKeyAuth\": {\n            \"type\": \"apiKey\",\n            \"name\": \"Authorization\",\n            \"in\": \"header\"\n        }\n    }\n}`\n\n// SwaggerInfo holds exported Swagger Info so clients can modify it\nvar SwaggerInfo = &swag.Spec{\n\tVersion:          \"\",\n\tHost:             \"\",\n\tBasePath:         \"/\",\n\tSchemes:          []string{},\n\tTitle:            \"Apache Answer\",\n\tDescription:      \"Apache Answer API\",\n\tInfoInstanceName: \"swagger\",\n\tSwaggerTemplate:  docTemplate,\n\tLeftDelim:        \"{{\",\n\tRightDelim:       \"}}\",\n}\n\nfunc init() {\n\tswag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)\n}\n"
  },
  {
    "path": "docs/release/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n============================================================================\n   APACHE ANSWER SUBCOMPONENTS:\n\n   The Apache answer project contains subcomponents with separate copyright\n   notices and license terms. Your use of the source code for the these\n   subcomponents is subject to the terms and conditions of the following\n   licenses.\n\n========================================================================\nApache 2.0 licenses\n========================================================================\n\nThe following components are provided under the Apache 2.0 License.\n\n   (Apache License, Version 2.0) react-helmet-async (https://github.com/staylor/react-helmet-async) [link](./licenses/LICENSE-staylor-react-helmet-async.txt)\n   (Apache License, Version 2.0) golang-mock (https://github.com/golang/mock) [link](./licenses/LICENSE-golang-mock.txt)\n   (Apache License, Version 2.0) google-wire (https://github.com/google/wire) [link](./licenses/LICENSE-google-wire.txt)\n   (Apache License, Version 2.0) mojocn-base64Captcha (https://github.com/mojocn/base64Captcha) [link](./licenses/LICENSE-mojocn-base64Captcha.txt)\n   (Apache License, Version 2.0) ory-dockertest (https://github.com/ory/dockertest) [link](./licenses/LICENSE-ory-dockertest.txt)\n   (Apache License, Version 2.0) spf13-cobra (https://github.com/spf13/cobra) [link](./licenses/LICENSE-spf13-cobra.txt)\n\n========================================================================\nMIT licenses\n========================================================================\n\nThe following components are provided under the MIT License. See project link for details.\n\n   (MIT License) axios (https://github.com/axios/axios) [link](./licenses/LICENSE-axios-axios.txt)\n   (MIT License) bootstrap (https://github.com/twbs/bootstrap) [link](./licenses/LICENSE-twbs-bootstrap.txt)\n   (MIT License) icons (https://github.com/twbs/icons) [link](./licenses/LICENSE-twbs-icons.txt)\n   (MIT License) classnames (https://github.com/JedWatson/classnames) [link](./LICENSE-JedWatson-classnames.txt)\n   (MIT License) codemirror (https://github.com/codemirror/basic-setup) [link](./licenses/LICENSE-codemirror-basic-setup.txt)\n   (MIT License) @codemirror/lang-markdown (https://github.com/codemirror/lang-markdown) [link](./licenses/LICENSE-codemirror-lang-markdown.txt)\n   (MIT License) @codemirror/language-data (https://github.com/codemirror/language-data) [link](./licenses/LICENSE-codemirror-language-data.txt)\n   (MIT License) @codemirror/state (https://github.com/codemirror/state) [link](./licenses/LICENSE-codemirror-state.txt)\n   (MIT License) @codemirror/view (https://github.com/codemirror/view) [link](./licenses/LICENSE-codemirror-view.txt)\n   (MIT License) color (https://github.com/Qix-/color) [link](./licenses/LICENSE-Qix--color.txt)\n   (MIT License) copy-to-clipboard (https://github.com/sudodoki/copy-to-clipboard) [link](./licenses/LICENSE-sudodoki-copy-to-clipboard.txt)\n   (MIT License) dayjs (https://github.com/iamkun/dayjs) [link](./licenses/LICENSE-iamkun-dayjs.txt)\n   (MIT License) i18next (https://github.com/i18next/i18next) [link](./licenses/LICENSE-i18next-i18next.txt)\n   (MIT License) lodash (https://github.com/lodash/lodash) [link](./licenses/LICENSE-lodash-lodash.txt)\n   (MIT License) marked (https://github.com/markedjs/marked) [link](./licenses/LICENSE-markedjs-marked.txt)\n   (MIT License) next-share (https://github.com/Bunlong/next-share) [link](./licenses/LIcENSE-Bunlong-next-share.txt)\n   (MIT License) node-qrcode (https://github.com/soldair/node-qrcode) [link](./licenses/LICENSE-soldair-qrcode.txt)\n   (MIT License) react (https://github.com/facebook/react) [link](./licenses/LICENSE-facebook-react.txt)\n   (MIT License) react-bootstrap (https://github.com/react-bootstrap/react-bootstrap) [link](./licenses/LICENSE-react-bootstrap-react-bootstrap.txt)\n   (MIT License) react-i18next (https://github.com/i18next/react-i18next) [link](./licenses/LICENSE-i18next-react-i18next.txt)\n   (MIT License) react-router (https://github.com/remix-run/react-router) [link](./licenses/LICENSE-remix-run-react-router.txt)\n   (MIT License) swr (https://github.com/vercel/swr) [link](./licenses/LICENSE-vercel-swr.txt)\n   (MIT License) zustand (https://github.com/pmndrs/zustand) [link](./licenses/LICENSE-pmndrs-zustand.txt)\n   (MIT License) mozillazg-go-pinyin (https://github.com/mozillazg/go-pinyin) [link](./licenses/LICENSE-mozillazg-go-pinyin.txt)\n   (MIT License) Machiel-slugify (https://github.com/Machiel/slugify) [link](./licenses/LICENSE-Machiel-slugify.txt)\n   (MIT License) Masterminds-semver (https://github.com/Masterminds/semver) [link](./licenses/LICENSE-Masterminds-semver.txt)\n   (MIT License) anargu-gin-brotli (https://github.com/anargu/gin-brotli) [link](./licenses/LICENSE-anargu-gin-brotli.txt)\n   (MIT License) asaskevich-govalidator (https://github.com/asaskevich/govalidator) [link](./licenses/LICENSE-asaskevich-govalidator.txt)\n   (MIT License) disintegration-imaging (https://github.com/disintegration/imaging) [link](./licenses/LICENSE-disintegration-imaging.txt)\n   (MIT License) gin-gonic-gin (https://github.com/gin-gonic/gin) [link](./licenses/LICENSE-gin-gonic-gin.txt)\n   (MIT License) go-playground-locales (https://github.com/go-playground/locales) [link](./licenses/LICENSE-go-playground-locales.txt)\n   (MIT License) go-playground-universal-translator (https://github.com/go-playground/universal-translator) [link](./licenses/LICENSE-go-playground-universal-translator.txt)\n   (MIT License) go-playground-validator (https://github.com/go-playground/validator) [link](./licenses/LICENSE-go-playground-validator.txt)\n   (MIT License) goccy-go-json (https://github.com/goccy/go-json) [link](./licenses/LICENSE-goccy-go-json.txt)\n   (MIT License) jinzhu-copier (https://github.com/jinzhu/copier) [link](./licenses/LICENSE-jinzhu-copier.txt)\n   (MIT License) jinzhu-now (https://github.com/jinzhu/now) [link](./licenses/LICENSE-jinzhu-now.txt)\n   (MIT License) jordan-wright-email (https://github.com/jordan-wright/email) [link](./licenses/LICENSE-jordan-wright-email.txt)\n   (MIT License) lib-pq (https://github.com/lib/pq) [link](./licenses/LICENSE-lib-pq.txt)\n   (MIT License) mattn-go-sqlite3 (https://github.com/mattn/go-sqlite3) [link](./licenses/LICENSE-mattn-go-sqlite3.txt)\n   (MIT License) segmentfault-pacman (https://github.com/segmentfault/pacman) [link](./licenses/LICENSE-segmentfault-pacman.txt)\n   (MIT License) robfig-cron (https://github.com/robfig/cron) [link](./licenses/LICENSE-robfig-cron.txt)\n   (MIT License) scottleedavis-go-exif-remove (https://github.com/scottleedavis/go-exif-remove) [link](./licenses/LICENSE-scottleedavis-go-exif-remove.txt)\n   (MIT License) stretchr-testify (https://github.com/stretchr/testify) [link](./licenses/LICENSE-stretchr-testify.txt)\n   (MIT License) swaggo-files (https://github.com/swaggo/files) [link](./licenses/LICENSE-swaggo-files.txt)\n   (MIT License) swaggo-gin-swagger (https://github.com/swaggo/gin-swagger) [link](./licenses/LICENSE-swaggo-gin-swagger.txt)\n   (MIT License) swaggo-swag (https://github.com/swaggo/swag) [link](./licenses/LICENSE-swaggo-swag.txt)\n   (MIT License) tidwall-gjson (https://github.com/tidwall/gjson) [link](./licenses/LICENSE-tidwall-gjson.txt)\n   (MIT License) yuin-goldmark (https://github.com/yuin/goldmark) [link](./licenses/LICENSE-yuin-goldmark.txt)\n   (MIT License) go-gomail-gomail (https://gopkg.in/gomail.v2) [link](./licenses/LICENSE-go-gomail-gomail.txt)\n   (MIT License) front-matter (https://github.com/jxson/front-matter) [link](./licenses/LICENSE-jxson-front-matter.txt)\n   (MIT License) js-sha256 (https://github.com/emn178/js-sha256) [link](./licenses/LICENSE-emn178-js-sha256.txt)\n\n========================================================================\nBSD licenses\n========================================================================\n\nThe following components are provided under a BSD license. See project link for details.\n\n   (BSD 2-Clause) bwmarrin-snowflake (https://github.com/bwmarrin/snowflake) [link](./licenses/LICENSE-bwmarrin-snowflake.txt)\n   (BSD 2-Clause) xorm (https://xorm.io/xorm) [link](./licenses/LICENSE-xorm.txt)\n   (BSD 3-Clause) google-uuid (https://github.com/google/uuid) [link](./licenses/LICENSE-google-uuid.txt)\n   (BSD 3-Clause) grokify-html-strip-tags-go (https://github.com/grokify/html-strip-tags-go) [link](./licenses/LICENSE-grokify-html-strip-tags-go.txt)\n   (BSD 3-Clause) microcosm-cc-bluemonday (https://github.com/microcosm-cc/bluemonday) [link](./licenses/LICENSE-microcosm-cc-bluemonday.txt)\n   (BSD 3-Clause) cznic-sqlite (https://modernc.org/sqlite) [link](./licenses/LICENSE-cznic-sqlite.txt)\n   (BSD 3-Clause) jsdiff (https://github.com/kpdecker/jsdiff) [link](./licenses/LICENSE-kpdecker-jsdiff.txt)\n   (BSD 3-Clause) qs (https://github.com/ljharb/qs) [link](./licenses/LICENSE-ljharb-qs.txt)\n\n========================================================================\nISC licenses\n========================================================================\n\nThe following components are provided under a ISC license. See project link for details.\n\n   (ISC License) node-semver (https://github.com/npm/node-semver) [link](./licenses/LICENSE-npm-node-semver.txt)\n\n========================================================================\nMIT and Apache-2.0 licenses\n========================================================================\n\nThe following components are provided under a MIT and Apache-2.0 license. See project link for details.\n\n    (MIT and Apache-2.0) go-yaml-yaml (https://gopkg.in/yaml.v3) [link](./licenses/LICENSE-go-yaml-yaml.txt)\n\n========================================================================\nMPL licenses\n========================================================================\n\nThe following components are provided under a MPL license. See project link for details.\n\n    (MPL-2.0) go-sql-driver-mysql (https://github.com/go-sql-driver/mysql) [link](./licenses/LICENSE-go-sql-driver-mysql.txt)\n"
  },
  {
    "path": "docs/release/NOTICE",
    "content": "Apache Answer\nCopyright 2025 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (https://www.apache.org/).\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-JedWatson-classnames.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2018 Jed Watson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-Machiel-slugify.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Machiel Molenaar\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-Masterminds-semver.txt",
    "content": "Copyright (C) 2014-2019, Matt Butcher and Matt Farina\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-Qix--color.txt",
    "content": "Copyright (c) 2012 Heather Arthur\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-anargu-gin-brotli.txt",
    "content": "MIT License\n\nCopyright (c) 2021 Anthony Arostegui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-asaskevich-govalidator.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014-2020 Alex Saskevich\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "docs/release/licenses/LICENSE-axios-axios.txt",
    "content": "# Copyright (c) 2014-present Matt Zabriskie & Collaborators\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-bwmarrin-snowflake.txt",
    "content": "Copyright (c) 2016, Bruce\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-codemirror-basic-setup.txt",
    "content": "MIT License\n\nCopyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-codemirror-lang-markdown.txt",
    "content": "MIT License\n\nCopyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-codemirror-language-data.txt",
    "content": "MIT License\n\nCopyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-codemirror-state.txt",
    "content": "MIT License\n\nCopyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-codemirror-view.txt",
    "content": "MIT License\n\nCopyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-cznic-sqlite.txt",
    "content": "Copyright (c) 2017 The Sqlite Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors\nmay be used to endorse or promote products derived from this software without\nspecific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-disintegration-imaging.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2012 Grigory Dryapak\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-emn178-js-sha256.txt",
    "content": "Copyright (c) 2014-2024 Chen, Yi-Cyuan\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-facebook-react.txt",
    "content": "MIT License\n\nCopyright (c) Meta Platforms, Inc. and affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-gin-gonic-gin.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Manuel Martínez-Almeida\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-gomail-gomail.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-playground-locales.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Go Playground\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "docs/release/licenses/LICENSE-go-playground-universal-translator.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Go Playground\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-playground-validator.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Dean Karn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-resty-resty.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-sql-driver-mysql.txt",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in \n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-go-yaml-yaml.txt",
    "content": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n    apic.go emitterc.go parserc.go readerc.go scannerc.go\n    writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-goccy-go-json.txt",
    "content": "MIT License\n\nCopyright (c) 2020 Masaaki Goshima\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-golang-org-x.txt",
    "content": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "docs/release/licenses/LICENSE-google-uuid.txt",
    "content": "Copyright (c) 2009,2014 Google Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-google-wire.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-grokify-html-strip-tags-go.txt",
    "content": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-i18next-i18next.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 i18next\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-i18next-react-i18next.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 i18next\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-iamkun-dayjs.txt",
    "content": "MIT License\n\nCopyright (c) 2018-present, iamkun\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-jinzhu-copier.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Jinzhu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-jinzhu-now.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013-NOW  Jinzhu <wosmvp@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-joho-godotenv.txt",
    "content": "Copyright (c) 2013 John Barton\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-jordan-wright-email.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Jordan Wright\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-jxson-front-matter.txt",
    "content": "# The MIT License (MIT)\n\nCopyright (c) Jason Campbell (\"Author\")\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-kpdecker-jsdiff.txt",
    "content": "Software License Agreement (BSD License)\n\nCopyright (c) 2009-2015, Kevin Decker <kpdecker@gmail.com>\n\nAll rights reserved.\n\nRedistribution and use of this software in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above\n  copyright notice, this list of conditions and the\n  following disclaimer.\n\n* Redistributions in binary form must reproduce the above\n  copyright notice, this list of conditions and the\n  following disclaimer in the documentation and/or other\n  materials provided with the distribution.\n\n* Neither the name of Kevin Decker nor the names of its\n  contributors may be used to endorse or promote products\n  derived from this software without specific prior\n  written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR\nIMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER\nIN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT\nOF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-lib-pq.txt",
    "content": "Copyright (c) 2011-2013, 'pq' Contributors\nPortions Copyright (C) 2011 Blake Mizerany\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-ljharb-qs.txt",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-lodash-lodash.txt",
    "content": "The MIT License\n\nCopyright JS Foundation and other contributors <https://js.foundation/>\n\nBased on Underscore.js, copyright Jeremy Ashkenas,\nDocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>\n\nThis software consists of voluntary contributions made by many\nindividuals. For exact contribution history, see the revision history\navailable at https://github.com/lodash/lodash\n\nThe following license applies to all parts of this software except as\ndocumented below:\n\n====\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n====\n\nCopyright and related rights for sample code are waived via CC0. Sample\ncode is defined as all source code displayed within the prose of the\ndocumentation.\n\nCC0: http://creativecommons.org/publicdomain/zero/1.0/\n\n====\n\nFiles located in the node_modules and vendor directories are externally\nmaintained libraries used by this software which have their own\nlicenses; we recommend you read them, as their terms may differ from the\nterms above.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-mark3labs-mcp-go.txt",
    "content": "MIT License\n\nCopyright (c) 2024 Anthropic, PBC\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-markedjs-marked.txt",
    "content": "# License information\n\n## Contribution License Agreement\n\nIf you contribute code to this project, you are implicitly allowing your code\nto be distributed under the MIT license. You are also implicitly verifying that\nall code is your original work. `</legalese>`\n\n## Marked\n\nCopyright (c) 2018+, MarkedJS (https://github.com/markedjs/)\nCopyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n## Markdown\n\nCopyright © 2004, John Gruber\nhttp://daringfireball.net/\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nThis software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-mattn-go-sqlite3.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Yasuhiro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-microcosm-cc-bluemonday.txt",
    "content": "SPDX short identifier: BSD-3-Clause\nhttps://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2014, David Kitchen <david@buro9.com>\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the organisation (Microcosm) nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-mojocn-base64Captcha.txt",
    "content": "Copyright 2019 Eric neochau@gmail.com\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "docs/release/licenses/LICENSE-mozillazg-go-pinyin.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 mozillazg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-npm-node-semver.txt",
    "content": "The ISC License\n\nCopyright (c) Isaac Z. Schlueter and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-ory-dockertest.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-pmndrs-zustand.txt",
    "content": "MIT License\n\nCopyright (c) 2019 Paul Henschel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-react-bootstrap-react-bootstrap.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014-present Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-remix-run-react-router.txt",
    "content": "MIT License\n\nCopyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-robfig-cron.txt",
    "content": "Copyright (C) 2012 Rob Figueiredo\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-sashabaranov-go-openai.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-scottleedavis-go-exif-remove.txt",
    "content": "MIT License\n\nCopyright (c) 2019 scott lee davis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-segmentfault-pacman.txt",
    "content": "MIT License\n\nCopyright (c) since 2022 The Segmentfault Development Team.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-soldair-qrcode.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2012 Ryan Day\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-spf13-cobra.txt",
    "content": "                                Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-staylor-react-helmet-async.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2018 The New York Times Company\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-stretchr-testify.txt",
    "content": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-sudodoki-copy-to-clipboard.txt",
    "content": "MIT License\n\nCopyright (c) 2017 sudodoki <smd.deluzion@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-swaggo-files.txt",
    "content": "MIT License\n\nCopyright (c) 2019 Swaggo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-swaggo-gin-swagger.txt",
    "content": "MIT License\n\nCopyright (c) 2017 Swaggo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-swaggo-swag.txt",
    "content": "MIT License\n\nCopyright (c) 2017 Eason Lin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-tidwall-gjson.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Josh Baker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-twbs-bootstrap.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2011-2023 The Bootstrap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-twbs-icons.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2019-2023 The Bootstrap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-uber-go-mock.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-vercel-swr.txt",
    "content": "MIT License\n\nCopyright (c) 2023 Vercel, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-xorm.txt",
    "content": "Copyright (c) 2013 - 2015 The Xorm Authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "docs/release/licenses/LICENSE-yuin-goldmark.txt",
    "content": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "docs/release/licenses/LIcENSE-Bunlong-next-share.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 Bunlong\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "docs/swagger.json",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"Apache Answer API\",\n        \"title\": \"Apache Answer\",\n        \"contact\": {}\n    },\n    \"basePath\": \"/\",\n    \"paths\": {\n        \"/\": {\n            \"get\": {\n                \"description\": \"if config file not exist try to redirect to install page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"if config file not exist try to redirect to install page\",\n                \"responses\": {}\n            }\n        },\n        \"/answer/admin/api/ai-config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteAIResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update AI configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update AI configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AI config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteAIReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai-models\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI models\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI models\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAIModelResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai-provider\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get AI provider configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get AI provider configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAIProviderResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai/conversation\": {\n            \"get\": {\n                \"description\": \"get conversation detail for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"get conversation detail for admin\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"conversation id\",\n                        \"name\": \"conversation_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AIConversationAdminDetailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"description\": \"delete conversation and its related records for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"delete conversation for admin\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AIConversationAdminDeleteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/ai/conversation/page\": {\n            \"get\": {\n                \"description\": \"get conversation list for admin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation-admin\"\n                ],\n                \"summary\": \"get conversation list for admin\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.AIConversationAdminListItem\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/answer/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Status:[available,deleted,pending]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"AdminAnswerPage admin answer page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"available\",\n                            \"deleted\",\n                            \"pending\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"answer id or question title\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/answer/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update answer status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update answer status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AdminUpdateAnswerStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AdminUpdateAnswerStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/api-key\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AddAPIKeyResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete apikey\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"delete apikey\",\n                \"parameters\": [\n                    {\n                        \"description\": \"apikey\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.DeleteAPIKeyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/api-key/all\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all api keys\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get all api keys\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetAPIKeyResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/badge/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update badge status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminBadge\"\n                ],\n                \"summary\": \"update badge status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdateBadgeStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateBadgeStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/badges\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list all badges by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminBadge\"\n                ],\n                \"summary\": \"list all badges by page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"\",\n                            \"active\",\n                            \"inactive\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"badge status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"search param\",\n                        \"name\": \"q\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetBadgeListPagedResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/dashboard\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"DashboardInfo\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"DashboardInfo\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/delete/permanently\": {\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete permanently\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"delete permanently\",\n                \"parameters\": [\n                    {\n                        \"description\": \"DeletePermanentlyReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.DeletePermanentlyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/language/options\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"Get language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/mcp-config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get MCP configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get MCP configuration\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteMCPResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update MCP configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update MCP configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"MCP config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteMCPReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugin/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"get plugin config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"plugin_slug_name\",\n                        \"name\": \"plugin_slug_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPluginConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"update plugin config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePluginConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugin/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update plugin status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"update plugin status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePluginStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/plugins\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"AdminPlugin\"\n                ],\n                \"summary\": \"get plugin list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"status: active/inactive\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"have config\",\n                        \"name\": \"have_config\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/question/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Status:[available,closed,deleted,pending]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"AdminQuestionPage admin question page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"available\",\n                            \"closed\",\n                            \"deleted\",\n                            \"pending\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question id or title\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/question/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update question status\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AdminUpdateQuestionStatusReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AdminUpdateQuestionStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/reasons\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reasons by object type and action\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"reason\"\n                ],\n                \"summary\": \"get reasons by object type and action\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"comment\",\n                            \"user\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object_type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"status\",\n                            \"close\",\n                            \"flag\",\n                            \"review\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/roles\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get role list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get role list\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetRoleResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/setting/privileges\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetPrivilegesConfig get privileges config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"GetPrivilegesConfig get privileges config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPrivilegesConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update privileges config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update privileges config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdatePrivilegesConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/setting/smtp\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetSMTPConfig get smtp config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"GetSMTPConfig get smtp config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetSMTPConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update smtp config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update smtp config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"smtp config\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateSMTPConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/advanced\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site advanced setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site advanced setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteAdvancedResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site advanced info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site advanced info\",\n                \"parameters\": [\n                    {\n                        \"description\": \"advanced settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteAdvancedReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/branding\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteBrandingResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info branding\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info branding\",\n                \"parameters\": [\n                    {\n                        \"description\": \"branding info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteBrandingReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/custom-css-html\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info custom html css config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info custom html css config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site custom css html config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site custom css html config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/general\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site general information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site general information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteGeneralResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site general information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site general information\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteGeneralReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/interface\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteInterfaceSettingsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info interface\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteInterfaceReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/login\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info login config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info login config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site login\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteLoginReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/polices\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get the policies information for the site\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get the policies information for the site\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SitePoliciesResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site policies configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site policies configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"write info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SitePoliciesReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/question\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site questions setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site questions setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteQuestionsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site question settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site question settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"questions settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteQuestionsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/security\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get the security information for the site\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get the security information for the site\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteSecurityResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site security configuration\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site security configuration\",\n                \"parameters\": [\n                    {\n                        \"description\": \"write info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteSecurityReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/seo\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site seo information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site seo information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteSeoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site seo information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site seo information\",\n                \"parameters\": [\n                    {\n                        \"description\": \"seo\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteSeoReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/tag\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site tags setting\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site tags setting\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteTagsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site tag settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site tag settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tags settings\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteTagsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/theme\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site info theme config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site info theme config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteThemeResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site custom css html config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site custom css html config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"login info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteThemeReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/users\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site user config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site user config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteUsersResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info config about users\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info config about users\",\n                \"parameters\": [\n                    {\n                        \"description\": \"users info\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteUsersReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/siteinfo/users-settings\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get site interface\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get site interface\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteUsersSettingsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update site info users settings\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update site info users settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"general\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SiteUsersSettingsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/theme/options\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Get theme options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"Get theme options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddUserReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/activation\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user activation\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get user activation\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user id\",\n                        \"name\": \"user_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserActivationResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/password\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user password\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserPasswordReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/profile\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"edit user profile\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"edit user profile\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.EditUserProfileReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/role\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user role\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user role\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserRoleReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/user/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"update user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserStatusReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add users\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"add users\",\n                \"parameters\": [\n                    {\n                        \"description\": \"user\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddUsersReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users/activation\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"send user activation\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"send user activation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"SendUserActivationReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.SendUserActivationReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/admin/api/users/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"admin\"\n                ],\n                \"summary\": \"get user page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"search query: email, username or id:[id]\",\n                        \"name\": \"query\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"staff user\",\n                        \"name\": \"staff\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"suspended\",\n                            \"deleted\",\n                            \"inactive\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"user status\",\n                        \"name\": \"status\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"records\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUserPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/activity/timeline\": {\n            \"get\": {\n                \"description\": \"get object timeline\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get object timeline\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag slug name\",\n                        \"name\": \"tag_slug_name\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"tag\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"is show vote\",\n                        \"name\": \"show_vote\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetObjectTimelineResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/activity/timeline/detail\": {\n            \"get\": {\n                \"description\": \"get object timeline detail\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get object timeline detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"revision id\",\n                        \"name\": \"revision_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetObjectTimelineResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation\": {\n            \"get\": {\n                \"description\": \"get conversation detail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"get conversation detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"conversation id\",\n                        \"name\": \"conversation_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.AIConversationDetailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation/page\": {\n            \"get\": {\n                \"description\": \"get conversation list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"get conversation list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.AIConversationListItem\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/ai/conversation/vote\": {\n            \"post\": {\n                \"description\": \"vote record\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"ai-conversation\"\n                ],\n                \"summary\": \"vote record\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote request\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AIConversationVoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Update Answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Update Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AnswerUpdateReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AnswerUpdateReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Add Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"add answer request\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AnswerAddReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"delete answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"answer\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/acceptance\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Accept Answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Accept Answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"AcceptAnswerReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AcceptAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/info\": {\n            \"get\": {\n                \"description\": \"Get Answer Detail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"Get Answer Detail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetAnswerInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/page\": {\n            \"get\": {\n                \"description\": \"AnswerList \\u003cbr\\u003e \\u003cb\\u003eorder\\u003c/b\\u003e (default or updated)\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"AnswerList\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"question_id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/answer/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover the deleted answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Answer\"\n                ],\n                \"summary\": \"recover answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"answer\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RecoverAnswerReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge\": {\n            \"get\": {\n                \"description\": \"get badge info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get badge info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetBadgeInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/awards/page\": {\n            \"get\": {\n                \"description\": \"get badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"badge id\",\n                        \"name\": \"badge_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"only list the award by username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetBadgeInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/user/awards\": {\n            \"get\": {\n                \"description\": \"get user badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get user badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user name\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserBadgeAwardListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badge/user/awards/recent\": {\n            \"get\": {\n                \"description\": \"get user badge award list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"get user badge award list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"user name\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserBadgeAwardListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/badges\": {\n            \"get\": {\n                \"description\": \"list all badges group by group\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"api-badge\"\n                ],\n                \"summary\": \"list all badges group by group\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetBadgeListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/collection/switch\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add collection\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Collection\"\n                ],\n                \"summary\": \"add collection\",\n                \"parameters\": [\n                    {\n                        \"description\": \"collection\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.CollectionSwitchReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.CollectionSwitchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/comment\": {\n            \"get\": {\n                \"description\": \"get comment by id\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get comment by id\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"update comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"add comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"remove comment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"remove comment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"comment\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveCommentReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/comment/page\": {\n            \"get\": {\n                \"description\": \"get comment page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"get comment page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"vote\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"query condition\",\n                        \"name\": \"query_cond\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/binding/email\": {\n            \"post\": {\n                \"description\": \"external login binding user send email\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"external login binding user send email\",\n                \"parameters\": [\n                    {\n                        \"description\": \"external login binding user send email\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ExternalLoginBindingUserSendEmailReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ExternalLoginBindingUserSendEmailResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all enabled connectors\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"get all enabled connectors\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.ConnectorInfoResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get all connectors info about user\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"get all connectors info about user\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.ConnectorUserInfoResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/connector/user/unbinding\": {\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"unbind external user login\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginConnector\"\n                ],\n                \"summary\": \"unbind external user login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"ExternalLoginUnbindingReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ExternalLoginUnbindingReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/embed/config\": {\n            \"get\": {\n                \"description\": \"get embed plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Plugin\"\n                ],\n                \"summary\": \"get embed plugin config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/plugin.EmbedConfig\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/file\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"upload file\",\n                \"consumes\": [\n                    \"multipart/form-data\"\n                ],\n                \"tags\": [\n                    \"Upload\"\n                ],\n                \"summary\": \"upload file\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"post\",\n                            \"post_attachment\",\n                            \"avatar\",\n                            \"branding\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"identify the source of the file upload\",\n                        \"name\": \"source\",\n                        \"in\": \"formData\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"file\",\n                        \"description\": \"file\",\n                        \"name\": \"file\",\n                        \"in\": \"formData\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"string\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/follow\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"follow object or cancel follow operation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"follow object or cancel follow operation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"follow\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.FollowReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.FollowResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/follow/tags\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user follow tags\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"update user follow tags\",\n                \"parameters\": [\n                    {\n                        \"description\": \"follow\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateFollowTagsReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/language/config\": {\n            \"get\": {\n                \"description\": \"get language config mapping\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get language config mapping\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Accept-Language\",\n                        \"name\": \"Accept-Language\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/language/options\": {\n            \"get\": {\n                \"description\": \"Get language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"Get language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/meta/reaction\": {\n            \"get\": {\n                \"description\": \"get reaction for an object\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Meta\"\n                ],\n                \"summary\": \"get reaction\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object_id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ReactionRespItem\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update reaction. if not exist, add one\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Meta\"\n                ],\n                \"summary\": \"add or update reaction\",\n                \"parameters\": [\n                    {\n                        \"description\": \"reaction\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateReactionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get notification list\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"get notification list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"inbox\",\n                            \"achievement\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"type\",\n                        \"name\": \"type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"all\",\n                            \"posts\",\n                            \"invites\",\n                            \"votes\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"inbox_type\",\n                        \"name\": \"inbox_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/read/state\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ClearUnRead\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"ClearUnRead\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearIDRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearIDRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/read/state/all\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ClearUnRead\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"ClearUnRead\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/notification/status\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetRedDot\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"GetRedDot\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"DelRedDot\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Notification\"\n                ],\n                \"summary\": \"DelRedDot\",\n                \"parameters\": [\n                    {\n                        \"description\": \"NotificationClearRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.NotificationClearRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/permission\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"check user permission\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Permission\"\n                ],\n                \"summary\": \"check user permission\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"question.add\",\n                            \"question.edit\",\n                            \"question.edit_without_review\",\n                            \"question.delete\",\n                            \"question.close\",\n                            \"question.reopen\",\n                            \"question.vote_up\",\n                            \"question.vote_down\",\n                            \"question.pin\",\n                            \"question.unpin\",\n                            \"question.hide\",\n                            \"question.show\",\n                            \"answer.add\",\n                            \"answer.edit\",\n                            \"answer.edit_without_review\",\n                            \"answer.delete\",\n                            \"answer.accept\",\n                            \"answer.vote_up\",\n                            \"answer.vote_down\",\n                            \"answer.invite_someone_to_answer\",\n                            \"comment.add\",\n                            \"comment.edit\",\n                            \"comment.delete\",\n                            \"comment.vote_up\",\n                            \"comment.vote_down\",\n                            \"report.add\",\n                            \"tag.add\",\n                            \"tag.edit\",\n                            \"tag.edit_slug_name\",\n                            \"tag.edit_without_review\",\n                            \"tag.delete\",\n                            \"tag.synonym\",\n                            \"link.url_limit\",\n                            \"vote.detail\",\n                            \"answer.audit\",\n                            \"question.audit\",\n                            \"tag.audit\",\n                            \"tag.use_reserved_tag\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"permission key\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"object\",\n                                            \"additionalProperties\": {\n                                                \"type\": \"boolean\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/answer/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal answers\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Personal\"\n                ],\n                \"summary\": \"list personal answers\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"score\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/collection/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal collections\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Collection\"\n                ],\n                \"summary\": \"list personal collections\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/comment/page\": {\n            \"get\": {\n                \"description\": \"user personal comment list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Comment\"\n                ],\n                \"summary\": \"user personal comment list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetCommentPersonalWithPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/qa/top\": {\n            \"get\": {\n                \"description\": \"UserTop\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"UserTop\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/rank/page\": {\n            \"get\": {\n                \"description\": \"user personal rank list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Rank\"\n                ],\n                \"summary\": \"user personal rank list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetRankPersonalPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"GetOtherUserInfoByUsername\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"GetOtherUserInfoByUsername\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetOtherUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/personal/vote/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user personal votes\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"get user personal votes\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetVoteWithPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/plugin/status\": {\n            \"get\": {\n                \"description\": \"get all plugins status\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Plugin\"\n                ],\n                \"summary\": \"get all plugins status\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/post/render\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"render post content\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Upload\"\n                ],\n                \"summary\": \"render post content\",\n                \"parameters\": [\n                    {\n                        \"description\": \"PostRenderReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.PostRenderReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"update question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionUpdate\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"add question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionAdd\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"delete question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/answer\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add question and answer\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"add question and answer\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionAddByAnswer\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/info\": {\n            \"get\": {\n                \"description\": \"get question details\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question details\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"1\",\n                        \"description\": \"Question TagID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/invite\": {\n            \"get\": {\n                \"description\": \"get question invite user info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question invite user info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"1\",\n                        \"description\": \"Question ID\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update question invite user\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"update question invite user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionUpdateInviteUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/link\": {\n            \"get\": {\n                \"description\": \"get question link\",\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get question link\",\n                \"parameters\": [\n                    {\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"in_days\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"active\",\n                            \"hot\",\n                            \"score\",\n                            \"unanswered\",\n                            \"recommend\",\n                            \"frequent\"\n                        ],\n                        \"type\": \"string\",\n                        \"name\": \"order\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"maximum\": 100,\n                        \"minimum\": 1,\n                        \"type\": \"integer\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/operation\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Operation question \\\\n operation [pin unpin hide show]\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Operation question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.OperationQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/page\": {\n            \"get\": {\n                \"description\": \"get questions by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get questions by page\",\n                \"parameters\": [\n                    {\n                        \"description\": \"QuestionPageReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionPageReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/recommend/page\": {\n            \"get\": {\n                \"description\": \"get recommend questions by page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"get recommend questions by page\",\n                \"parameters\": [\n                    {\n                        \"description\": \"QuestionPageReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionPageReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.QuestionPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover deleted question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"recover deleted question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.QuestionRecoverReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/reopen\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"reopen question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"reopen question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ReopenQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/similar\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"fuzzy query similar questions based on title\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"fuzzy query similar questions based on title\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"title\",\n                        \"name\": \"title\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/similar/tag\": {\n            \"get\": {\n                \"description\": \"Search Similar Question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Search Similar Question\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"question_id\",\n                        \"name\": \"question_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/status\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"Close question\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Question\"\n                ],\n                \"summary\": \"Close question\",\n                \"parameters\": [\n                    {\n                        \"description\": \"question\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.CloseQuestionReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/question/tags\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get tag list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag\",\n                        \"name\": \"tag\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetTagBasicResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/reasons\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reasons by object type and action\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"reason\"\n                ],\n                \"summary\": \"get reasons by object type and action\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"question\",\n                            \"answer\",\n                            \"comment\",\n                            \"user\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"object_type\",\n                        \"name\": \"object_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"status\",\n                            \"close\",\n                            \"flag\",\n                            \"review\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/render/config\": {\n            \"get\": {\n                \"description\": \"GetRenderConfig\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"PluginRender\"\n                ],\n                \"summary\": \"GetRenderConfig\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/plugin.RenderConfig\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add report \\u003cbr\\u003e source (question, answer, comment, user)\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"add report\",\n                \"parameters\": [\n                    {\n                        \"description\": \"report\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddReportReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report/review\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"review report\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"review report\",\n                \"parameters\": [\n                    {\n                        \"description\": \"flag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.ReviewReportReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/report/unreviewed/post\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed report post page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Report\"\n                ],\n                \"summary\": \"get unreviewed report post page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetReportListPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/review/pending/post\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update review\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Review\"\n                ],\n                \"summary\": \"update review\",\n                \"parameters\": [\n                    {\n                        \"description\": \"review\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateReviewReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/review/pending/post/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed post page\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Review\"\n                ],\n                \"summary\": \"get unreviewed post page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object_id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUnreviewedPostPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/reviewing/type\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get reviewing type\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get reviewing type\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetReviewingTypeResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions\": {\n            \"get\": {\n                \"description\": \"get revision list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get revision list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"object id\",\n                        \"name\": \"object_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetRevisionResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/audit\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"revision audit operation:approve or reject\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"revision audit\",\n                \"parameters\": [\n                    {\n                        \"description\": \"audit\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RevisionAuditReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/edit/check\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"check can update revision\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"check can update revision\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"id\",\n                        \"name\": \"id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/revisions/unreviewed\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get unreviewed revision list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Revision\"\n                ],\n                \"summary\": \"get unreviewed revision list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page id\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetUnreviewedRevisionResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/search\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"search object\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Search\"\n                ],\n                \"summary\": \"search object\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"query string\",\n                        \"name\": \"q\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"active\",\n                            \"score\",\n                            \"relevance\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SearchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/search/desc\": {\n            \"get\": {\n                \"description\": \"get search description\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Search\"\n                ],\n                \"summary\": \"get search description\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SearchResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/siteinfo\": {\n            \"get\": {\n                \"description\": \"get site info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site info\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.SiteInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/siteinfo/legal\": {\n            \"get\": {\n                \"description\": \"get site legal info\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site legal info\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"tos\",\n                            \"privacy\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"legal information type\",\n                        \"name\": \"info_type\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetSiteLegalInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag\": {\n            \"get\": {\n                \"description\": \"get tag one\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag one\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag id\",\n                        \"name\": \"tag_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"tag name\",\n                        \"name\": \"tag_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetTagResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"update tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"add tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"delete tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"delete tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RemoveTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/merge\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"merge tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"merge tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.AddTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/recover\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"recover delete tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"recover delete tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.RecoverTagReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/synonym\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update tag\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"update tag\",\n                \"parameters\": [\n                    {\n                        \"description\": \"tag\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateTagSynonymReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tag/synonyms\": {\n            \"get\": {\n                \"description\": \"get tag synonyms\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag synonyms\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"tag id\",\n                        \"name\": \"tag_id\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetTagSynonymsResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags\": {\n            \"get\": {\n                \"description\": \"get tags list by slug name\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tags list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        },\n                        \"collectionFormat\": \"csv\",\n                        \"description\": \"string collection\",\n                        \"name\": \"tags\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetTagBasicResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags/following\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get following tag list\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get following tag list\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetFollowingTagsResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/tags/page\": {\n            \"get\": {\n                \"description\": \"get tag page\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Tag\"\n                ],\n                \"summary\": \"get tag page\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"description\": \"page size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"slug_name\",\n                        \"name\": \"slug_name\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"enum\": [\n                            \"popular\",\n                            \"name\",\n                            \"newest\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"query condition\",\n                        \"name\": \"query_cond\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"allOf\": [\n                                                {\n                                                    \"$ref\": \"#/definitions/pager.PageModel\"\n                                                },\n                                                {\n                                                    \"type\": \"object\",\n                                                    \"properties\": {\n                                                        \"list\": {\n                                                            \"type\": \"array\",\n                                                            \"items\": {\n                                                                \"$ref\": \"#/definitions/schema.GetTagPageResp\"\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            ]\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/action/record\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"ActionRecord\",\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"ActionRecord\",\n                \"parameters\": [\n                    {\n                        \"enum\": [\n                            \"login\",\n                            \"e_mail\",\n                            \"find_pass\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"action\",\n                        \"name\": \"action\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.ActionRecordResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"user change email verification\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"user change email verification\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserChangeEmailVerifyReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserChangeEmailVerifyReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/change/code\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"send email to the user email then change their email\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"send email to the user email then change their email\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserChangeEmailSendCodeReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserChangeEmailSendCodeReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/verification\": {\n            \"post\": {\n                \"description\": \"UserVerifyEmail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserVerifyEmail\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"code\",\n                        \"name\": \"code\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/email/verification/send\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserVerifyEmailSend\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserVerifyEmailSend\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"captcha_id\",\n                        \"name\": \"captcha_id\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"\",\n                        \"description\": \"captcha_code\",\n                        \"name\": \"captcha_code\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/info\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user info, if user no login response http code is 200, but user info is null\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"GetUserInfoByUserID\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetCurrentLoginUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserUpdateInfo update user info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserUpdateInfo update user info\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"UpdateInfoRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateInfoRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/info/search\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"SearchUserListByName\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"SearchUserListByName\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetOtherUserInfoResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/interface\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserUpdateInterface update user interface config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserUpdateInterface update user interface config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"access-token\",\n                        \"name\": \"Authorization\",\n                        \"in\": \"header\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"UpdateInfoRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserInterfaceRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/login/email\": {\n            \"post\": {\n                \"description\": \"UserEmailLogin\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserEmailLogin\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserEmailLogin\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserEmailLoginReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/logout\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"user logout\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"user logout\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/notification/config\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user's notification config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"update user's notification config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdateUserNotificationConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserNotificationConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user's notification config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user's notification config\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserNotificationConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/notification/unsubscribe\": {\n            \"put\": {\n                \"description\": \"unsubscribe notification\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"unsubscribe notification\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserUnsubscribeNotificationReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserUnsubscribeNotificationReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password\": {\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"UserModifyPassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserModifyPassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserModifyPasswordReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserModifyPasswordReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password/replacement\": {\n            \"post\": {\n                \"description\": \"UseRePassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UseRePassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRePassWordRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRePassWordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/password/reset\": {\n            \"post\": {\n                \"description\": \"RetrievePassWord\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"RetrievePassWord\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRetrievePassWordRequest\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRetrievePassWordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/plugin/config\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get user plugin config\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"get user plugin config\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"plugin_slug_name\",\n                        \"name\": \"plugin_slug_name\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetPluginConfigResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"update user plugin config\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"update user plugin config\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UpdatePluginConfigReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UpdateUserPluginConfigReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/plugin/configs\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"get plugin list that used for user.\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"UserPlugin\"\n                ],\n                \"summary\": \"get plugin list that used for user.\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/schema.GetUserPluginListResp\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/ranking\": {\n            \"get\": {\n                \"description\": \"get user ranking\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user ranking\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserRankingResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/register/email\": {\n            \"post\": {\n                \"description\": \"UserRegisterByEmail\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"UserRegisterByEmail\",\n                \"parameters\": [\n                    {\n                        \"description\": \"UserRegisterReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.UserRegisterReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.UserLoginResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/user/staff\": {\n            \"get\": {\n                \"description\": \"get user staff\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"User\"\n                ],\n                \"summary\": \"get user staff\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.GetUserStaffResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/vote/down\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add vote\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"vote down\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.VoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.VoteResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/answer/api/v1/vote/up\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"add vote\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Activity\"\n                ],\n                \"summary\": \"vote up\",\n                \"parameters\": [\n                    {\n                        \"description\": \"vote\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/schema.VoteReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/schema.VoteResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/custom.css\": {\n            \"get\": {\n                \"description\": \"get site custom CSS\",\n                \"produces\": [\n                    \"text/css\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site custom CSS\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/base-info\": {\n            \"post\": {\n                \"description\": \"init base info\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"init base info\",\n                \"parameters\": [\n                    {\n                        \"description\": \"InitBaseInfoReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.InitBaseInfoReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/config-file/check\": {\n            \"post\": {\n                \"description\": \"check config file if exist when installation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"check config file if exist when installation\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/install.CheckConfigFileResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/db/check\": {\n            \"post\": {\n                \"description\": \"check database if exist when installation\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"check database if exist when installation\",\n                \"parameters\": [\n                    {\n                        \"description\": \"CheckDatabaseReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.CheckDatabaseReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"$ref\": \"#/definitions/install.CheckConfigFileResp\"\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/init\": {\n            \"post\": {\n                \"description\": \"init environment\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"installation\"\n                ],\n                \"summary\": \"init environment\",\n                \"parameters\": [\n                    {\n                        \"description\": \"CheckDatabaseReq\",\n                        \"name\": \"data\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/install.CheckDatabaseReq\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/language/config\": {\n            \"get\": {\n                \"description\": \"get installation language config mapping\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get installation language config mapping\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"installation language\",\n                        \"name\": \"lang\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/installation/language/options\": {\n            \"get\": {\n                \"description\": \"get installation language options\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Lang\"\n                ],\n                \"summary\": \"get installation language options\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"allOf\": [\n                                {\n                                    \"$ref\": \"#/definitions/handler.RespBody\"\n                                },\n                                {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"data\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/definitions/translator.LangOption\"\n                                            }\n                                        }\n                                    }\n                                }\n                            ]\n                        }\n                    }\n                }\n            }\n        },\n        \"/personal/question/page\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"ApiKeyAuth\": []\n                    }\n                ],\n                \"description\": \"list personal questions\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Personal\"\n                ],\n                \"summary\": \"list personal questions\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"string\",\n                        \"description\": \"username\",\n                        \"name\": \"username\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"enum\": [\n                            \"newest\",\n                            \"score\"\n                        ],\n                        \"type\": \"string\",\n                        \"description\": \"order\",\n                        \"name\": \"order\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"0\",\n                        \"description\": \"page\",\n                        \"name\": \"page\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"default\": \"20\",\n                        \"description\": \"page_size\",\n                        \"name\": \"page_size\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/handler.RespBody\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/robots.txt\": {\n            \"get\": {\n                \"description\": \"get site robots information\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"site\"\n                ],\n                \"summary\": \"get site robots information\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"constant.NotificationChannelKey\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"email\"\n            ],\n            \"x-enum-varnames\": [\n                \"EmailChannel\"\n            ]\n        },\n        \"constant.Privilege\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"key\": {\n                    \"type\": \"string\"\n                },\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                }\n            }\n        },\n        \"entity.BadgeLevel\": {\n            \"type\": \"integer\",\n            \"enum\": [\n                1,\n                2,\n                3\n            ],\n            \"x-enum-varnames\": [\n                \"BadgeLevelBronze\",\n                \"BadgeLevelSilver\",\n                \"BadgeLevelGold\"\n            ]\n        },\n        \"handler.RespBody\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"description\": \"http code\",\n                    \"type\": \"integer\"\n                },\n                \"data\": {\n                    \"description\": \"response data\"\n                },\n                \"msg\": {\n                    \"description\": \"response message\",\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"description\": \"reason key\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"install.CheckConfigFileResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config_file_exist\": {\n                    \"type\": \"boolean\"\n                },\n                \"db_connection_success\": {\n                    \"type\": \"boolean\"\n                },\n                \"db_table_exist\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"install.CheckDatabaseReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"db_type\"\n            ],\n            \"properties\": {\n                \"db_file\": {\n                    \"type\": \"string\"\n                },\n                \"db_host\": {\n                    \"type\": \"string\"\n                },\n                \"db_name\": {\n                    \"type\": \"string\"\n                },\n                \"db_password\": {\n                    \"type\": \"string\"\n                },\n                \"db_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"postgres\",\n                        \"sqlite3\",\n                        \"mysql\"\n                    ]\n                },\n                \"db_username\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_cert\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"ssl_key\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_mode\": {\n                    \"type\": \"string\"\n                },\n                \"ssl_root_cert\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"install.InitBaseInfoReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"email\",\n                \"external_content_display\",\n                \"lang\",\n                \"name\",\n                \"password\",\n                \"site_name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"lang\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"site_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"pager.PageModel\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"type\": \"integer\"\n                },\n                \"list\": {}\n            }\n        },\n        \"plugin.EmbedConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"platform\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"plugin.RenderConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"select_theme\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminDeleteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"conversation_id\"\n            ],\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminDetailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"records\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.AIConversationRecord\"\n                    }\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.AIConversationUserInfo\"\n                }\n            }\n        },\n        \"schema.AIConversationAdminListItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"helpful_count\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"unhelpful_count\": {\n                    \"type\": \"integer\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.AIConversationUserInfo\"\n                }\n            }\n        },\n        \"schema.AIConversationDetailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"records\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.AIConversationRecord\"\n                    }\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AIConversationListItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"conversation_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"topic\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationRecord\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"chat_completion_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"helpful\": {\n                    \"type\": \"integer\"\n                },\n                \"role\": {\n                    \"type\": \"string\"\n                },\n                \"unhelpful\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AIConversationUserInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AIConversationVoteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"chat_completion_id\",\n                \"vote_type\"\n            ],\n            \"properties\": {\n                \"cancel\": {\n                    \"type\": \"boolean\"\n                },\n                \"chat_completion_id\": {\n                    \"type\": \"string\"\n                },\n                \"vote_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"helpful\",\n                        \"unhelpful\"\n                    ]\n                }\n            }\n        },\n        \"schema.AIPromptConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"en_us\": {\n                    \"type\": \"string\"\n                },\n                \"zh_cn\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AcceptAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30\n                }\n            }\n        },\n        \"schema.ActObjectInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ActObjectTimeline\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"activity_id\": {\n                    \"type\": \"string\"\n                },\n                \"activity_type\": {\n                    \"type\": \"string\"\n                },\n                \"cancelled\": {\n                    \"type\": \"boolean\"\n                },\n                \"cancelled_at\": {\n                    \"type\": \"integer\"\n                },\n                \"comment\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"revision_id\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                }\n            }\n        },\n        \"schema.ActionRecordResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_img\": {\n                    \"type\": \"string\"\n                },\n                \"verify\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.AddAPIKeyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"description\",\n                \"scope\"\n            ],\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150\n                },\n                \"scope\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"read-only\",\n                        \"global\"\n                    ]\n                }\n            }\n        },\n        \"schema.AddAPIKeyResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_key\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AddCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\",\n                \"original_text\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"mention_username_list\": {\n                    \"description\": \"@ user id list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 600,\n                    \"minLength\": 2\n                },\n                \"reply_comment_id\": {\n                    \"description\": \"reply comment id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AddReportReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\",\n                \"report_type\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"report content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\",\n                    \"maxLength\": 20\n                },\n                \"report_type\": {\n                    \"description\": \"report type\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.AddTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"original_text\",\n                \"slug_name\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                }\n            }\n        },\n        \"schema.AddUserReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"email\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.AddUsersReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"users\": {\n                    \"description\": \"users info line by line\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AdminUpdateAnswerStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"available\",\n                        \"deleted\"\n                    ]\n                }\n            }\n        },\n        \"schema.AdminUpdateQuestionStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"available\",\n                        \"closed\",\n                        \"deleted\"\n                    ]\n                }\n            }\n        },\n        \"schema.AnswerAddReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"content\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AnswerInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted\": {\n                    \"type\": \"integer\"\n                },\n                \"collected\": {\n                    \"type\": \"boolean\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"create_time\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"question_info\": {\n                    \"$ref\": \"#/definitions/schema.QuestionInfoResp\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"update_time\": {\n                    \"type\": \"integer\"\n                },\n                \"update_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AnswerUpdateReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"content\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"edit_summary\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.AvatarInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 200\n                },\n                \"gravatar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 200\n                },\n                \"type\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.BadgeListInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"earned_count\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.BadgeStatus\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"active\",\n                \"inactive\"\n            ],\n            \"x-enum-varnames\": [\n                \"BadgeStatusActive\",\n                \"BadgeStatusInactive\"\n            ]\n        },\n        \"schema.CloseQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"close_msg\": {\n                    \"description\": \"close_type\",\n                    \"type\": \"string\"\n                },\n                \"close_type\": {\n                    \"description\": \"close_type\",\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.CollectionSwitchReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"group_id\",\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"bookmark\": {\n                    \"type\": \"boolean\"\n                },\n                \"group_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.CollectionSwitchResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object_collection_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.ConfigField\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ConfigFieldOption\"\n                    }\n                },\n                \"required\": {\n                    \"type\": \"boolean\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"ui_options\": {\n                    \"$ref\": \"#/definitions/schema.ConfigFieldUIOptions\"\n                },\n                \"value\": {}\n            }\n        },\n        \"schema.ConfigFieldOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConfigFieldUIOptions\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"action\": {\n                    \"$ref\": \"#/definitions/schema.UIOptionAction\"\n                },\n                \"class_name\": {\n                    \"type\": \"string\"\n                },\n                \"field_class_name\": {\n                    \"type\": \"string\"\n                },\n                \"input_type\": {\n                    \"type\": \"string\"\n                },\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"placeholder\": {\n                    \"type\": \"string\"\n                },\n                \"rows\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                },\n                \"variant\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConnectorInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ConnectorUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"binding\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_id\": {\n                    \"type\": \"string\"\n                },\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.DeleteAPIKeyReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.DeletePermanentlyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"type\"\n            ],\n            \"properties\": {\n                \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"users\",\n                        \"questions\",\n                        \"answers\"\n                    ]\n                }\n            }\n        },\n        \"schema.EditUserProfileReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"display_name\",\n                \"email\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                }\n            }\n        },\n        \"schema.ExternalLoginBindingUserSendEmailReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"binding_key\",\n                \"email\"\n            ],\n            \"properties\": {\n                \"binding_key\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"must\": {\n                    \"description\": \"If must is true, whatever email if exists, try to bind user.\\nIf must is false, when email exist, will only be prompted with a warning.\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.ExternalLoginBindingUserSendEmailResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"type\": \"string\"\n                },\n                \"email_exist_and_must_be_confirmed\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.ExternalLoginUnbindingReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_id\"\n            ],\n            \"properties\": {\n                \"external_id\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.FollowReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"is_cancel\": {\n                    \"description\": \"is cancel\",\n                    \"type\": \"boolean\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.FollowResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"follows\": {\n                    \"description\": \"the followers of object\",\n                    \"type\": \"integer\"\n                },\n                \"is_followed\": {\n                    \"description\": \"if user is followed object will be true,otherwise false\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.GetAIModelResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"object\": {\n                    \"type\": \"string\"\n                },\n                \"owned_by\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAIProviderResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"default_api_host\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAPIKeyResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_key\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"last_used_at\": {\n                    \"type\": \"integer\"\n                },\n                \"scope\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetAnswerInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.AnswerInfo\"\n                },\n                \"question\": {\n                    \"$ref\": \"#/definitions/schema.QuestionInfoResp\"\n                }\n            }\n        },\n        \"schema.GetBadgeInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"badge description\",\n                    \"type\": \"string\"\n                },\n                \"earned_count\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"is_single\": {\n                    \"description\": \"badge is single or multiple\",\n                    \"type\": \"boolean\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetBadgeListPagedResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"award_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"badge description\",\n                    \"type\": \"string\"\n                },\n                \"earned\": {\n                    \"description\": \"badge earned count\",\n                    \"type\": \"boolean\"\n                },\n                \"group_name\": {\n                    \"description\": \"badge group name\",\n                    \"type\": \"string\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"badge status\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.BadgeStatus\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.GetBadgeListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"badges\": {\n                    \"description\": \"badge list info\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.BadgeListInfo\"\n                    }\n                },\n                \"group_name\": {\n                    \"description\": \"badge group name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetCommentPersonalWithPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetCommentResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"is_vote\": {\n                    \"description\": \"current user if already vote this comment\",\n                    \"type\": \"boolean\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"description\": \"parsed comment content\",\n                    \"type\": \"string\"\n                },\n                \"reply_comment_id\": {\n                    \"description\": \"reply comment id\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_display_name\": {\n                    \"description\": \"reply user display name\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_id\": {\n                    \"description\": \"reply user id\",\n                    \"type\": \"string\"\n                },\n                \"reply_user_status\": {\n                    \"description\": \"reply user status\",\n                    \"type\": \"string\"\n                },\n                \"reply_username\": {\n                    \"description\": \"reply user username\",\n                    \"type\": \"string\"\n                },\n                \"user_avatar\": {\n                    \"description\": \"user avatar\",\n                    \"type\": \"string\"\n                },\n                \"user_display_name\": {\n                    \"description\": \"user display name\",\n                    \"type\": \"string\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"user_status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"vote_count\": {\n                    \"description\": \"user vote amount\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetCurrentLoginUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"description\": \"access token\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"authority_group\": {\n                    \"description\": \"authority group\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"$ref\": \"#/definitions/schema.AvatarInfo\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follow count\",\n                    \"type\": \"integer\"\n                },\n                \"have_password\": {\n                    \"description\": \"user have password\",\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mail_status\": {\n                    \"description\": \"mail status(1 pass 2 to be verified)\",\n                    \"type\": \"integer\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"notice_status\": {\n                    \"description\": \"notice status(1 on 2off)\",\n                    \"type\": \"integer\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"visit_token\": {\n                    \"description\": \"visit token\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetFollowingTagsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetObjectTimelineResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object_info\": {\n                    \"$ref\": \"#/definitions/schema.ActObjectInfo\"\n                },\n                \"timeline\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ActObjectTimeline\"\n                    }\n                }\n            }\n        },\n        \"schema.GetOtherUserInfoByUsernameResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"email\\nfollow count\",\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"status_msg\": {\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetOtherUserInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.GetOtherUserInfoByUsernameResp\"\n                }\n            }\n        },\n        \"schema.GetPluginConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ConfigField\"\n                    }\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetPluginListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"have_config\": {\n                    \"type\": \"boolean\"\n                },\n                \"link\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetPrivilegesConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PrivilegeOption\"\n                    }\n                },\n                \"selected_level\": {\n                    \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                }\n            }\n        },\n        \"schema.GetRankPersonalPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"rank_type\": {\n                    \"description\": \"rank type\",\n                    \"type\": \"string\"\n                },\n                \"reputation\": {\n                    \"description\": \"reputation\",\n                    \"type\": \"integer\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetReportListPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"author_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"flag_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"comment\"\n                    ]\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"$ref\": \"#/definitions/schema.ReasonItem\"\n                },\n                \"reason_content\": {\n                    \"type\": \"string\"\n                },\n                \"submit_at\": {\n                    \"type\": \"integer\"\n                },\n                \"submitter_user\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetReviewingTypeResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"todo_amount\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetRevisionResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content\": {},\n                \"create_at\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"use_id\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                }\n            }\n        },\n        \"schema.GetRoleResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetSMTPConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"encryption\": {\n                    \"description\": \"\\\"\\\" SSL TLS\",\n                    \"type\": \"string\"\n                },\n                \"from_email\": {\n                    \"type\": \"string\"\n                },\n                \"from_name\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_authentication\": {\n                    \"type\": \"boolean\"\n                },\n                \"smtp_host\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_password\": {\n                    \"type\": \"string\"\n                },\n                \"smtp_port\": {\n                    \"type\": \"integer\"\n                },\n                \"smtp_username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetSiteLegalInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetTagBasicResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetTagPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"description\": \"created time\",\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"description\": \"description\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\"\n                },\n                \"excerpt\": {\n                    \"description\": \"excerpt\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follower amount\",\n                    \"type\": \"integer\"\n                },\n                \"is_follower\": {\n                    \"description\": \"is follower\",\n                    \"type\": \"boolean\"\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"description\": \"parsed_text\",\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"description\": \"question amount\",\n                    \"type\": \"integer\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"description\": \"updated time\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetTagResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"excerpt\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"is_follower\": {\n                    \"type\": \"boolean\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"member_actions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_count\": {\n                    \"type\": \"integer\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"type\": \"string\"\n                },\n                \"updated_at\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.GetTagSynonymsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"synonyms\": {\n                    \"description\": \"synonyms\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagSynonym\"\n                    }\n                }\n            }\n        },\n        \"schema.GetUnreviewedPostPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"author_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_status\": {\n                    \"type\": \"integer\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"comment\"\n                    ]\n                },\n                \"original_text\": {\n                    \"type\": \"string\"\n                },\n                \"parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"reason\": {\n                    \"type\": \"string\"\n                },\n                \"review_id\": {\n                    \"type\": \"integer\"\n                },\n                \"submit_at\": {\n                    \"type\": \"integer\"\n                },\n                \"submitter_display_name\": {\n                    \"type\": \"string\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUnreviewedRevisionResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"info\": {\n                    \"$ref\": \"#/definitions/schema.UnreviewedRevisionInfoInfo\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"unreviewed_info\": {\n                    \"$ref\": \"#/definitions/schema.GetRevisionResp\"\n                }\n            }\n        },\n        \"schema.GetUserActivationResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"activation_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserBadgeAwardListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"earned_count\": {\n                    \"description\": \"badge award count\",\n                    \"type\": \"integer\"\n                },\n                \"icon\": {\n                    \"description\": \"badge icon\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"description\": \"badge level\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/entity.BadgeLevel\"\n                        }\n                    ]\n                },\n                \"name\": {\n                    \"description\": \"badge name\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserNotificationConfigResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"all_new_question\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"all_new_question_for_following_tags\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"inbox\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                }\n            }\n        },\n        \"schema.GetUserPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"deleted_at\": {\n                    \"description\": \"delete time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"role_name\": {\n                    \"description\": \"role name\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"user status(normal,suspended,deleted,inactive)\",\n                    \"type\": \"string\"\n                },\n                \"suspended_at\": {\n                    \"description\": \"suspended time\",\n                    \"type\": \"integer\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until time\",\n                    \"type\": \"integer\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserPluginListResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetUserStaffResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.GetVoteWithPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_id\": {\n                    \"description\": \"answer id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"object_id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"description\": \"object type\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"question\",\n                        \"answer\",\n                        \"tag\",\n                        \"comment\"\n                    ]\n                },\n                \"question_id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"title\": {\n                    \"description\": \"title\",\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"description\": \"url title\",\n                    \"type\": \"string\"\n                },\n                \"vote_type\": {\n                    \"description\": \"vote type\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.LoadingAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"state\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.NotificationChannelConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enable\": {\n                    \"type\": \"boolean\"\n                },\n                \"key\": {\n                    \"$ref\": \"#/definitions/constant.NotificationChannelKey\"\n                }\n            }\n        },\n        \"schema.NotificationClearIDRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.NotificationClearRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"type\"\n            ],\n            \"properties\": {\n                \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"inbox\",\n                        \"achievement\"\n                    ]\n                }\n            }\n        },\n        \"schema.OnCompleteAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"refresh_form_config\": {\n                    \"type\": \"boolean\"\n                },\n                \"toast_return_message\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.Operation\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"level\": {\n                    \"$ref\": \"#/definitions/schema.OperationLevel\"\n                },\n                \"msg\": {\n                    \"type\": \"string\"\n                },\n                \"time\": {\n                    \"type\": \"integer\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.OperationLevel\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"info\",\n                \"danger\",\n                \"warning\",\n                \"secondary\"\n            ],\n            \"x-enum-varnames\": [\n                \"OperationLevelInfo\",\n                \"OperationLevelDanger\",\n                \"OperationLevelWarning\",\n                \"OperationLevelSecondary\"\n            ]\n        },\n        \"schema.OperationQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"operation\": {\n                    \"description\": \"operation [pin unpin hide show]\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PermissionMemberAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"action\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PostRenderReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.PrivilegeLevel\": {\n            \"type\": \"integer\",\n            \"enum\": [\n                1,\n                2,\n                3,\n                99\n            ],\n            \"x-enum-varnames\": [\n                \"PrivilegeLevel1\",\n                \"PrivilegeLevel2\",\n                \"PrivilegeLevel3\",\n                \"PrivilegeLevelCustom\"\n            ]\n        },\n        \"schema.PrivilegeOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"level\": {\n                    \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                },\n                \"level_desc\": {\n                    \"type\": \"string\"\n                },\n                \"privileges\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/constant.Privilege\"\n                    }\n                }\n            }\n        },\n        \"schema.QuestionAdd\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"title\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionAddByAnswer\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_content\",\n                \"title\"\n            ],\n            \"properties\": {\n                \"answer_content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"mention_username_list\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answered\": {\n                    \"type\": \"boolean\"\n                },\n                \"collected\": {\n                    \"type\": \"boolean\"\n                },\n                \"collection_count\": {\n                    \"type\": \"integer\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"create_time\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"edit_time\": {\n                    \"type\": \"integer\"\n                },\n                \"extends_actions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"first_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"is_followed\": {\n                    \"type\": \"boolean\"\n                },\n                \"last_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"last_answered_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"member_actions\": {\n                    \"description\": \"MemberActions\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.PermissionMemberAction\"\n                    }\n                },\n                \"operation\": {\n                    \"$ref\": \"#/definitions/schema.Operation\"\n                },\n                \"pin\": {\n                    \"type\": \"integer\"\n                },\n                \"show\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"unique_view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"update_time\": {\n                    \"type\": \"integer\"\n                },\n                \"update_user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"$ref\": \"#/definitions/schema.UserBasicInfo\"\n                },\n                \"view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionPageReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"in_days\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"order\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"newest\",\n                        \"active\",\n                        \"hot\",\n                        \"score\",\n                        \"unanswered\",\n                        \"recommend\",\n                        \"frequent\"\n                    ]\n                },\n                \"page\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"page_size\": {\n                    \"type\": \"integer\",\n                    \"minimum\": 1\n                },\n                \"tag\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.QuestionPageResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted_answer_id\": {\n                    \"description\": \"answer information\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"collection_count\": {\n                    \"type\": \"integer\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"type\": \"integer\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"last_answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"operated_at\": {\n                    \"description\": \"operator information\",\n                    \"type\": \"integer\"\n                },\n                \"operation_type\": {\n                    \"type\": \"string\"\n                },\n                \"operator\": {\n                    \"$ref\": \"#/definitions/schema.QuestionPageRespOperator\"\n                },\n                \"pin\": {\n                    \"description\": \"1: unpin, 2: pin\",\n                    \"type\": \"integer\"\n                },\n                \"show\": {\n                    \"description\": \"0: show, 1: hide\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"unique_view_count\": {\n                    \"type\": \"integer\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"view_count\": {\n                    \"description\": \"question statistical information\",\n                    \"type\": \"integer\"\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.QuestionPageRespOperator\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionRecoverReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"question_id\"\n            ],\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.QuestionUpdate\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"title\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"description\": \"content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 0\n                },\n                \"edit_summary\": {\n                    \"description\": \"edit summary\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                },\n                \"invite_user\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"description\": \"question title\",\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.QuestionUpdateInviteUser\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"invite_user\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                }\n            }\n        },\n        \"schema.ReactionRespItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"description\": \"Count is the number of users who reacted\",\n                    \"type\": \"integer\"\n                },\n                \"emoji\": {\n                    \"description\": \"Emoji is the reaction emoji\",\n                    \"type\": \"string\"\n                },\n                \"is_active\": {\n                    \"description\": \"IsActive is if current user has reacted\",\n                    \"type\": \"boolean\"\n                },\n                \"tooltip\": {\n                    \"description\": \"Tooltip is the user's name who reacted\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReasonItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"content_type\": {\n                    \"type\": \"string\"\n                },\n                \"description\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"placeholder\": {\n                    \"type\": \"string\"\n                },\n                \"reason_key\": {\n                    \"type\": \"string\"\n                },\n                \"reason_type\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.RecoverAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"answer_id\"\n            ],\n            \"properties\": {\n                \"answer_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RecoverTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"tag_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveAnswerReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"comment_id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveQuestionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"captcha_id\",\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"description\": \"question id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.RemoveTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReopenQuestionReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"question_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ReviewReportReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"flag_id\",\n                \"operation_type\"\n            ],\n            \"properties\": {\n                \"close_msg\": {\n                    \"type\": \"string\"\n                },\n                \"close_type\": {\n                    \"type\": \"integer\"\n                },\n                \"content\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65535,\n                    \"minLength\": 6\n                },\n                \"flag_id\": {\n                    \"type\": \"string\"\n                },\n                \"operation_type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"edit_post\",\n                        \"close_post\",\n                        \"delete_post\",\n                        \"unlist_post\",\n                        \"ignore_report\"\n                    ]\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150,\n                    \"minLength\": 6\n                }\n            }\n        },\n        \"schema.RevisionAuditReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"operation\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"description\": \"object id\",\n                    \"type\": \"string\"\n                },\n                \"operation\": {\n                    \"description\": \"approve or reject\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SearchObject\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"excerpt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"Status\",\n                    \"type\": \"string\"\n                },\n                \"tags\": {\n                    \"description\": \"tags\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                },\n                \"user_info\": {\n                    \"description\": \"user info\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.SearchObjectUser\"\n                        }\n                    ]\n                },\n                \"vote_count\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SearchObjectUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SearchResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"count\": {\n                    \"type\": \"integer\"\n                },\n                \"list\": {\n                    \"description\": \"search response\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SearchResult\"\n                    }\n                }\n            }\n        },\n        \"schema.SearchResult\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"object\": {\n                    \"description\": \"this object\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.SearchObject\"\n                        }\n                    ]\n                },\n                \"object_type\": {\n                    \"description\": \"object_type\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SendUserActivationReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteAIProvider\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"api_host\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"api_key\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"model\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                }\n            }\n        },\n        \"schema.SiteAIReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_providers\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteAIProvider\"\n                    }\n                },\n                \"chosen_provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"prompt_config\": {\n                    \"$ref\": \"#/definitions/schema.AIPromptConfig\"\n                }\n            }\n        },\n        \"schema.SiteAIResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_providers\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteAIProvider\"\n                    }\n                },\n                \"chosen_provider\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50\n                },\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"prompt_config\": {\n                    \"$ref\": \"#/definitions/schema.AIPromptConfig\"\n                }\n            }\n        },\n        \"schema.SiteAdvancedReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"authorized_attachment_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"authorized_image_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"max_attachment_size\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_megapixel\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_size\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SiteAdvancedResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"authorized_attachment_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"authorized_image_extensions\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"max_attachment_size\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_megapixel\": {\n                    \"type\": \"integer\"\n                },\n                \"max_image_size\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.SiteBrandingReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"favicon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"mobile_logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"square_icon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteBrandingResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"favicon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"mobile_logo\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"square_icon\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteCustomCssHTMLReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom_css\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_footer\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_head\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_header\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_sidebar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                }\n            }\n        },\n        \"schema.SiteCustomCssHTMLResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"custom_css\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_footer\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_head\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_header\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                },\n                \"custom_sidebar\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 65536\n                }\n            }\n        },\n        \"schema.SiteGeneralReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 2000\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"short_description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteGeneralResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"contact_email\",\n                \"name\",\n                \"site_url\"\n            ],\n            \"properties\": {\n                \"contact_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                },\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 2000\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"short_description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"site_url\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 512\n                }\n            }\n        },\n        \"schema.SiteInfoResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"ai_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"branding\": {\n                    \"$ref\": \"#/definitions/schema.SiteBrandingResp\"\n                },\n                \"custom_css_html\": {\n                    \"$ref\": \"#/definitions/schema.SiteCustomCssHTMLResp\"\n                },\n                \"general\": {\n                    \"$ref\": \"#/definitions/schema.SiteGeneralResp\"\n                },\n                \"interface\": {\n                    \"$ref\": \"#/definitions/schema.SiteInterfaceSettingsResp\"\n                },\n                \"login\": {\n                    \"$ref\": \"#/definitions/schema.SiteLoginResp\"\n                },\n                \"mcp_enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"revision\": {\n                    \"type\": \"string\"\n                },\n                \"site_advanced\": {\n                    \"$ref\": \"#/definitions/schema.SiteAdvancedResp\"\n                },\n                \"site_legal\": {\n                    \"$ref\": \"#/definitions/schema.SiteLegalSimpleResp\"\n                },\n                \"site_questions\": {\n                    \"$ref\": \"#/definitions/schema.SiteQuestionsResp\"\n                },\n                \"site_security\": {\n                    \"$ref\": \"#/definitions/schema.SiteSecurityResp\"\n                },\n                \"site_seo\": {\n                    \"$ref\": \"#/definitions/schema.SiteSeoResp\"\n                },\n                \"site_tags\": {\n                    \"$ref\": \"#/definitions/schema.SiteTagsResp\"\n                },\n                \"site_users\": {\n                    \"$ref\": \"#/definitions/schema.SiteUsersResp\"\n                },\n                \"theme\": {\n                    \"$ref\": \"#/definitions/schema.SiteThemeResp\"\n                },\n                \"users_settings\": {\n                    \"$ref\": \"#/definitions/schema.SiteUsersSettingsResp\"\n                },\n                \"version\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteInterfaceReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"language\",\n                \"time_zone\"\n            ],\n            \"properties\": {\n                \"language\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"time_zone\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.SiteInterfaceSettingsResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"language\",\n                \"time_zone\"\n            ],\n            \"properties\": {\n                \"language\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                },\n                \"time_zone\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 128\n                }\n            }\n        },\n        \"schema.SiteLegalSimpleResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                }\n            }\n        },\n        \"schema.SiteLoginReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"allow_email_domains\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"allow_email_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_new_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_password_login\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteLoginResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"allow_email_domains\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"allow_email_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_new_registrations\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_password_login\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteMCPReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteMCPResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"http_header\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"type\": \"string\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SitePoliciesReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SitePoliciesResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"privacy_policy_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"privacy_policy_parsed_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_original_text\": {\n                    \"type\": \"string\"\n                },\n                \"terms_of_service_parsed_text\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteQuestionsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"min_content\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 0\n                },\n                \"min_tags\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 5,\n                    \"minimum\": 0\n                },\n                \"restrict_answer\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteQuestionsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"min_content\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 0\n                },\n                \"min_tags\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 5,\n                    \"minimum\": 0\n                },\n                \"restrict_answer\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSecurityReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"check_update\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSecurityResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"external_content_display\"\n            ],\n            \"properties\": {\n                \"check_update\": {\n                    \"type\": \"boolean\"\n                },\n                \"external_content_display\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"always_display\",\n                        \"ask_before_display\"\n                    ]\n                },\n                \"login_required\": {\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"schema.SiteSeoReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"permalink\",\n                \"robots\"\n            ],\n            \"properties\": {\n                \"permalink\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 4,\n                    \"minimum\": 0\n                },\n                \"robots\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteSeoResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"permalink\",\n                \"robots\"\n            ],\n            \"properties\": {\n                \"permalink\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 4,\n                    \"minimum\": 0\n                },\n                \"robots\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteTagsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"recommend_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                },\n                \"required_tag\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteTagsResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"recommend_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                },\n                \"required_tag\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved_tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.SiteWriteTag\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteThemeReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"theme\"\n            ],\n            \"properties\": {\n                \"color_scheme\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"layout\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"Full-width\",\n                        \"Fixed-width\"\n                    ]\n                },\n                \"theme\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 255\n                },\n                \"theme_config\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                }\n            }\n        },\n        \"schema.SiteThemeResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color_scheme\": {\n                    \"type\": \"string\"\n                },\n                \"layout\": {\n                    \"type\": \"string\"\n                },\n                \"theme\": {\n                    \"type\": \"string\"\n                },\n                \"theme_config\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"theme_options\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.ThemeOption\"\n                    }\n                }\n            }\n        },\n        \"schema.SiteUsersReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"allow_update_avatar\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_bio\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_display_name\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_location\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_username\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_website\": {\n                    \"type\": \"boolean\"\n                },\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"allow_update_avatar\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_bio\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_display_name\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_location\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_username\": {\n                    \"type\": \"boolean\"\n                },\n                \"allow_update_website\": {\n                    \"type\": \"boolean\"\n                },\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersSettingsReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteUsersSettingsResp\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"default_avatar\"\n            ],\n            \"properties\": {\n                \"default_avatar\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"system\",\n                        \"gravatar\"\n                    ]\n                },\n                \"gravatar_base_url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.SiteWriteTag\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"slug_name\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.TagItem\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                }\n            }\n        },\n        \"schema.TagResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"recommend\": {\n                    \"type\": \"boolean\"\n                },\n                \"reserved\": {\n                    \"type\": \"boolean\"\n                },\n                \"slug_name\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.TagSynonym\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"main_tag_slug_name\": {\n                    \"description\": \"if main tag slug name is not empty, this tag is synonymous with the main tag\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug name\",\n                    \"type\": \"string\"\n                },\n                \"tag_id\": {\n                    \"description\": \"tag id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.ThemeOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UIOptionAction\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"loading\": {\n                    \"$ref\": \"#/definitions/schema.LoadingAction\"\n                },\n                \"method\": {\n                    \"type\": \"string\"\n                },\n                \"on_complete\": {\n                    \"$ref\": \"#/definitions/schema.OnCompleteAction\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UnreviewedRevisionInfoInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"answer_accepted\": {\n                    \"type\": \"boolean\"\n                },\n                \"answer_count\": {\n                    \"type\": \"integer\"\n                },\n                \"answer_id\": {\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"type\": \"string\"\n                },\n                \"content\": {\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"type\": \"integer\"\n                },\n                \"html\": {\n                    \"type\": \"string\"\n                },\n                \"object_creator_user_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"object_type\": {\n                    \"type\": \"string\"\n                },\n                \"question_id\": {\n                    \"type\": \"string\"\n                },\n                \"show_status\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"integer\"\n                },\n                \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagResp\"\n                    }\n                },\n                \"title\": {\n                    \"type\": \"string\"\n                },\n                \"url_title\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateAPIKeyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"description\",\n                \"id\"\n            ],\n            \"properties\": {\n                \"description\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 150\n                },\n                \"id\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.UpdateBadgeStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"id\": {\n                    \"description\": \"badge id\",\n                    \"type\": \"string\"\n                },\n                \"status\": {\n                    \"description\": \"badge status\",\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.BadgeStatus\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateCommentReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"comment_id\",\n                \"original_text\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"description\": \"whether user can delete it\",\n                    \"type\": \"string\"\n                },\n                \"comment_id\": {\n                    \"description\": \"comment id\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original comment content\",\n                    \"type\": \"string\",\n                    \"maxLength\": 600,\n                    \"minLength\": 2\n                }\n            }\n        },\n        \"schema.UpdateFollowTagsReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"slug_name_list\": {\n                    \"description\": \"tag slug name list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                }\n            }\n        },\n        \"schema.UpdateInfoRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"$ref\": \"#/definitions/schema.AvatarInfo\"\n                },\n                \"bio\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 4096\n                },\n                \"display_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"location\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"website\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UpdatePluginConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdatePluginStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"enabled\": {\n                    \"type\": \"boolean\"\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdatePrivilegesConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"level\"\n            ],\n            \"properties\": {\n                \"custom_privileges\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/constant.Privilege\"\n                    }\n                },\n                \"level\": {\n                    \"minimum\": 1,\n                    \"allOf\": [\n                        {\n                            \"$ref\": \"#/definitions/schema.PrivilegeLevel\"\n                        }\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateReactionReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"emoji\",\n                \"object_id\",\n                \"reaction\"\n            ],\n            \"properties\": {\n                \"emoji\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"heart\",\n                        \"smile\",\n                        \"frown\"\n                    ]\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                },\n                \"reaction\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"activate\",\n                        \"deactivate\"\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateReviewReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"review_id\",\n                \"status\"\n            ],\n            \"properties\": {\n                \"review_id\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"approve\",\n                        \"reject\"\n                    ]\n                }\n            }\n        },\n        \"schema.UpdateSMTPConfigReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"encryption\": {\n                    \"description\": \"\\\"\\\" SSL TLS\",\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"SSL\",\n                        \"TLS\"\n                    ]\n                },\n                \"from_email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"from_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_authentication\": {\n                    \"type\": \"boolean\"\n                },\n                \"smtp_host\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"smtp_port\": {\n                    \"type\": \"integer\",\n                    \"maximum\": 65535,\n                    \"minimum\": 1\n                },\n                \"smtp_username\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 256\n                },\n                \"test_email_recipient\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateTagReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"display_name\": {\n                    \"description\": \"display_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"edit_summary\": {\n                    \"description\": \"edit summary\",\n                    \"type\": \"string\"\n                },\n                \"original_text\": {\n                    \"description\": \"original text\",\n                    \"type\": \"string\"\n                },\n                \"slug_name\": {\n                    \"description\": \"slug_name\",\n                    \"type\": \"string\",\n                    \"maxLength\": 35\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateTagSynonymReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"synonym_tag_list\",\n                \"tag_id\"\n            ],\n            \"properties\": {\n                \"synonym_tag_list\": {\n                    \"description\": \"synonym tag list\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.TagItem\"\n                    }\n                },\n                \"tag_id\": {\n                    \"description\": \"tag_id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserInterfaceRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"color_scheme\",\n                \"language\"\n            ],\n            \"properties\": {\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdateUserNotificationConfigReq\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"all_new_question\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"all_new_question_for_following_tags\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                },\n                \"inbox\": {\n                    \"$ref\": \"#/definitions/schema.NotificationChannelConfig\"\n                }\n            }\n        },\n        \"schema.UpdateUserPasswordReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"password\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserPluginConfigReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"plugin_slug_name\"\n            ],\n            \"properties\": {\n                \"config_fields\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {}\n                },\n                \"plugin_slug_name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                }\n            }\n        },\n        \"schema.UpdateUserRoleReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"role_id\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"user_id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UpdateUserStatusReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"status\",\n                \"user_id\"\n            ],\n            \"properties\": {\n                \"remove_all_content\": {\n                    \"type\": \"boolean\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"normal\",\n                        \"suspended\",\n                        \"deleted\",\n                        \"inactive\"\n                    ]\n                },\n                \"suspend_duration\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                        \"24h\",\n                        \"48h\",\n                        \"72h\",\n                        \"7d\",\n                        \"14d\",\n                        \"1m\",\n                        \"2m\",\n                        \"3m\",\n                        \"6m\",\n                        \"1y\",\n                        \"forever\"\n                    ]\n                },\n                \"user_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserBasicInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"type\": \"string\"\n                },\n                \"location\": {\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserChangeEmailSendCodeReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserChangeEmailVerifyReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UserEmailLoginReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserLoginResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access_token\": {\n                    \"description\": \"access token\",\n                    \"type\": \"string\"\n                },\n                \"answer_count\": {\n                    \"description\": \"answer count\",\n                    \"type\": \"integer\"\n                },\n                \"authority_group\": {\n                    \"description\": \"authority group\",\n                    \"type\": \"integer\"\n                },\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"bio\": {\n                    \"description\": \"bio markdown\",\n                    \"type\": \"string\"\n                },\n                \"bio_html\": {\n                    \"description\": \"bio html\",\n                    \"type\": \"string\"\n                },\n                \"color_scheme\": {\n                    \"description\": \"Color scheme\",\n                    \"type\": \"string\"\n                },\n                \"created_at\": {\n                    \"description\": \"create time\",\n                    \"type\": \"integer\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"description\": \"email\",\n                    \"type\": \"string\"\n                },\n                \"follow_count\": {\n                    \"description\": \"follow count\",\n                    \"type\": \"integer\"\n                },\n                \"have_password\": {\n                    \"description\": \"user have password\",\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"description\": \"user id\",\n                    \"type\": \"string\"\n                },\n                \"language\": {\n                    \"description\": \"language\",\n                    \"type\": \"string\"\n                },\n                \"last_login_date\": {\n                    \"description\": \"last login date\",\n                    \"type\": \"integer\"\n                },\n                \"location\": {\n                    \"description\": \"location\",\n                    \"type\": \"string\"\n                },\n                \"mail_status\": {\n                    \"description\": \"mail status(1 pass 2 to be verified)\",\n                    \"type\": \"integer\"\n                },\n                \"mobile\": {\n                    \"description\": \"mobile\",\n                    \"type\": \"string\"\n                },\n                \"notice_status\": {\n                    \"description\": \"notice status(1 on 2off)\",\n                    \"type\": \"integer\"\n                },\n                \"question_count\": {\n                    \"description\": \"question count\",\n                    \"type\": \"integer\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"role_id\": {\n                    \"description\": \"role id\",\n                    \"type\": \"integer\"\n                },\n                \"status\": {\n                    \"description\": \"user status\",\n                    \"type\": \"string\"\n                },\n                \"suspended_until\": {\n                    \"description\": \"suspended until timestamp\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"visit_token\": {\n                    \"description\": \"visit token\",\n                    \"type\": \"string\"\n                },\n                \"website\": {\n                    \"description\": \"website\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.UserModifyPasswordReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"old_pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserRankingResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"staffs\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                },\n                \"users_with_the_most_reputation\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                },\n                \"users_with_the_most_vote\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/schema.UserRankingSimpleInfo\"\n                    }\n                }\n            }\n        },\n        \"schema.UserRankingSimpleInfo\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"avatar\": {\n                    \"description\": \"avatar\",\n                    \"type\": \"string\"\n                },\n                \"display_name\": {\n                    \"description\": \"display name\",\n                    \"type\": \"string\"\n                },\n                \"rank\": {\n                    \"description\": \"rank\",\n                    \"type\": \"integer\"\n                },\n                \"username\": {\n                    \"description\": \"username\",\n                    \"type\": \"string\"\n                },\n                \"vote_count\": {\n                    \"description\": \"vote\",\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"schema.UserRePassWordRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 100\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32\n                }\n            }\n        },\n        \"schema.UserRegisterReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\",\n                \"name\",\n                \"pass\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 30,\n                    \"minLength\": 2\n                },\n                \"pass\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 32,\n                    \"minLength\": 8\n                }\n            }\n        },\n        \"schema.UserRetrievePassWordRequest\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"e_mail\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"e_mail\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.UserUnsubscribeNotificationReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"code\"\n            ],\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 500\n                }\n            }\n        },\n        \"schema.VoteReq\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"object_id\"\n            ],\n            \"properties\": {\n                \"captcha_code\": {\n                    \"type\": \"string\"\n                },\n                \"captcha_id\": {\n                    \"type\": \"string\"\n                },\n                \"is_cancel\": {\n                    \"type\": \"boolean\"\n                },\n                \"object_id\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"schema.VoteResp\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"down_votes\": {\n                    \"type\": \"integer\"\n                },\n                \"up_votes\": {\n                    \"type\": \"integer\"\n                },\n                \"vote_status\": {\n                    \"type\": \"string\"\n                },\n                \"votes\": {\n                    \"type\": \"integer\"\n                }\n            }\n        },\n        \"translator.LangOption\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"label\": {\n                    \"type\": \"string\"\n                },\n                \"progress\": {\n                    \"description\": \"Translation completion percentage\",\n                    \"type\": \"integer\"\n                },\n                \"value\": {\n                    \"type\": \"string\"\n                }\n            }\n        }\n    },\n    \"securityDefinitions\": {\n        \"ApiKeyAuth\": {\n            \"type\": \"apiKey\",\n            \"name\": \"Authorization\",\n            \"in\": \"header\"\n        }\n    }\n}"
  },
  {
    "path": "docs/swagger.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nbasePath: /\ndefinitions:\n  constant.NotificationChannelKey:\n    enum:\n    - email\n    type: string\n    x-enum-varnames:\n    - EmailChannel\n  constant.Privilege:\n    properties:\n      key:\n        type: string\n      label:\n        type: string\n      value:\n        minimum: 1\n        type: integer\n    type: object\n  entity.BadgeLevel:\n    enum:\n    - 1\n    - 2\n    - 3\n    type: integer\n    x-enum-varnames:\n    - BadgeLevelBronze\n    - BadgeLevelSilver\n    - BadgeLevelGold\n  handler.RespBody:\n    properties:\n      code:\n        description: http code\n        type: integer\n      data:\n        description: response data\n      msg:\n        description: response message\n        type: string\n      reason:\n        description: reason key\n        type: string\n    type: object\n  install.CheckConfigFileResp:\n    properties:\n      config_file_exist:\n        type: boolean\n      db_connection_success:\n        type: boolean\n      db_table_exist:\n        type: boolean\n    type: object\n  install.CheckDatabaseReq:\n    properties:\n      db_file:\n        type: string\n      db_host:\n        type: string\n      db_name:\n        type: string\n      db_password:\n        type: string\n      db_type:\n        enum:\n        - postgres\n        - sqlite3\n        - mysql\n        type: string\n      db_username:\n        type: string\n      ssl_cert:\n        type: string\n      ssl_enabled:\n        type: boolean\n      ssl_key:\n        type: string\n      ssl_mode:\n        type: string\n      ssl_root_cert:\n        type: string\n    required:\n    - db_type\n    type: object\n  install.InitBaseInfoReq:\n    properties:\n      contact_email:\n        maxLength: 500\n        type: string\n      email:\n        maxLength: 500\n        type: string\n      external_content_display:\n        enum:\n        - always_display\n        - ask_before_display\n        type: string\n      lang:\n        maxLength: 30\n        type: string\n      login_required:\n        type: boolean\n      name:\n        maxLength: 30\n        minLength: 2\n        type: string\n      password:\n        maxLength: 32\n        minLength: 8\n        type: string\n      site_name:\n        maxLength: 30\n        type: string\n      site_url:\n        maxLength: 512\n        type: string\n    required:\n    - contact_email\n    - email\n    - external_content_display\n    - lang\n    - name\n    - password\n    - site_name\n    - site_url\n    type: object\n  pager.PageModel:\n    properties:\n      count:\n        type: integer\n      list: {}\n    type: object\n  plugin.EmbedConfig:\n    properties:\n      enable:\n        type: boolean\n      platform:\n        type: string\n    type: object\n  plugin.RenderConfig:\n    properties:\n      select_theme:\n        type: string\n    type: object\n  schema.AIConversationAdminDeleteReq:\n    properties:\n      conversation_id:\n        type: string\n    required:\n    - conversation_id\n    type: object\n  schema.AIConversationAdminDetailResp:\n    properties:\n      conversation_id:\n        type: string\n      created_at:\n        type: integer\n      records:\n        items:\n          $ref: '#/definitions/schema.AIConversationRecord'\n        type: array\n      topic:\n        type: string\n      user_info:\n        $ref: '#/definitions/schema.AIConversationUserInfo'\n    type: object\n  schema.AIConversationAdminListItem:\n    properties:\n      created_at:\n        type: integer\n      helpful_count:\n        type: integer\n      id:\n        type: string\n      topic:\n        type: string\n      unhelpful_count:\n        type: integer\n      user_info:\n        $ref: '#/definitions/schema.AIConversationUserInfo'\n    type: object\n  schema.AIConversationDetailResp:\n    properties:\n      conversation_id:\n        type: string\n      created_at:\n        type: integer\n      records:\n        items:\n          $ref: '#/definitions/schema.AIConversationRecord'\n        type: array\n      topic:\n        type: string\n      updated_at:\n        type: integer\n    type: object\n  schema.AIConversationListItem:\n    properties:\n      conversation_id:\n        type: string\n      created_at:\n        type: integer\n      topic:\n        type: string\n    type: object\n  schema.AIConversationRecord:\n    properties:\n      chat_completion_id:\n        type: string\n      content:\n        type: string\n      created_at:\n        type: integer\n      helpful:\n        type: integer\n      role:\n        type: string\n      unhelpful:\n        type: integer\n    type: object\n  schema.AIConversationUserInfo:\n    properties:\n      avatar:\n        type: string\n      display_name:\n        type: string\n      id:\n        type: string\n      rank:\n        type: integer\n      username:\n        type: string\n    type: object\n  schema.AIConversationVoteReq:\n    properties:\n      cancel:\n        type: boolean\n      chat_completion_id:\n        type: string\n      vote_type:\n        enum:\n        - helpful\n        - unhelpful\n        type: string\n    required:\n    - chat_completion_id\n    - vote_type\n    type: object\n  schema.AIPromptConfig:\n    properties:\n      en_us:\n        type: string\n      zh_cn:\n        type: string\n    type: object\n  schema.AcceptAnswerReq:\n    properties:\n      answer_id:\n        type: string\n      question_id:\n        maxLength: 30\n        type: string\n    required:\n    - question_id\n    type: object\n  schema.ActObjectInfo:\n    properties:\n      answer_id:\n        type: string\n      display_name:\n        type: string\n      main_tag_slug_name:\n        type: string\n      object_type:\n        type: string\n      question_id:\n        type: string\n      title:\n        type: string\n      username:\n        type: string\n    type: object\n  schema.ActObjectTimeline:\n    properties:\n      activity_id:\n        type: string\n      activity_type:\n        type: string\n      cancelled:\n        type: boolean\n      cancelled_at:\n        type: integer\n      comment:\n        type: string\n      created_at:\n        type: integer\n      object_id:\n        type: string\n      object_type:\n        type: string\n      revision_id:\n        type: string\n      user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n    type: object\n  schema.ActionRecordResp:\n    properties:\n      captcha_id:\n        type: string\n      captcha_img:\n        type: string\n      verify:\n        type: boolean\n    type: object\n  schema.AddAPIKeyReq:\n    properties:\n      description:\n        maxLength: 150\n        type: string\n      scope:\n        enum:\n        - read-only\n        - global\n        type: string\n    required:\n    - description\n    - scope\n    type: object\n  schema.AddAPIKeyResp:\n    properties:\n      access_key:\n        type: string\n    type: object\n  schema.AddCommentReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      mention_username_list:\n        description: '@ user id list'\n        items:\n          type: string\n        type: array\n      object_id:\n        description: object id\n        type: string\n      original_text:\n        description: original comment content\n        maxLength: 600\n        minLength: 2\n        type: string\n      reply_comment_id:\n        description: reply comment id\n        type: string\n    required:\n    - object_id\n    - original_text\n    type: object\n  schema.AddReportReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      content:\n        description: report content\n        maxLength: 500\n        type: string\n      object_id:\n        description: object id\n        maxLength: 20\n        type: string\n      report_type:\n        description: report type\n        type: integer\n    required:\n    - object_id\n    - report_type\n    type: object\n  schema.AddTagReq:\n    properties:\n      display_name:\n        description: display_name\n        maxLength: 35\n        type: string\n      original_text:\n        description: original text\n        maxLength: 65536\n        type: string\n      slug_name:\n        description: slug_name\n        maxLength: 35\n        type: string\n    required:\n    - display_name\n    - original_text\n    - slug_name\n    type: object\n  schema.AddUserReq:\n    properties:\n      display_name:\n        maxLength: 30\n        minLength: 2\n        type: string\n      email:\n        maxLength: 500\n        type: string\n      password:\n        maxLength: 32\n        minLength: 8\n        type: string\n    required:\n    - display_name\n    - email\n    - password\n    type: object\n  schema.AddUsersReq:\n    properties:\n      users:\n        description: users info line by line\n        type: string\n    type: object\n  schema.AdminUpdateAnswerStatusReq:\n    properties:\n      answer_id:\n        type: string\n      status:\n        enum:\n        - available\n        - deleted\n        type: string\n    required:\n    - answer_id\n    - status\n    type: object\n  schema.AdminUpdateQuestionStatusReq:\n    properties:\n      question_id:\n        type: string\n      status:\n        enum:\n        - available\n        - closed\n        - deleted\n        type: string\n    required:\n    - question_id\n    - status\n    type: object\n  schema.AnswerAddReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      content:\n        maxLength: 65535\n        minLength: 6\n        type: string\n      question_id:\n        type: string\n    required:\n    - content\n    type: object\n  schema.AnswerInfo:\n    properties:\n      accepted:\n        type: integer\n      collected:\n        type: boolean\n      content:\n        type: string\n      create_time:\n        type: integer\n      html:\n        type: string\n      id:\n        type: string\n      member_actions:\n        description: MemberActions\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      question_id:\n        type: string\n      question_info:\n        $ref: '#/definitions/schema.QuestionInfoResp'\n      status:\n        type: integer\n      update_time:\n        type: integer\n      update_user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      vote_count:\n        type: integer\n      vote_status:\n        type: string\n    type: object\n  schema.AnswerUpdateReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      content:\n        maxLength: 65535\n        minLength: 6\n        type: string\n      edit_summary:\n        type: string\n      id:\n        type: string\n      title:\n        type: string\n    required:\n    - content\n    type: object\n  schema.AvatarInfo:\n    properties:\n      custom:\n        maxLength: 200\n        type: string\n      gravatar:\n        maxLength: 200\n        type: string\n      type:\n        maxLength: 100\n        type: string\n    type: object\n  schema.BadgeListInfo:\n    properties:\n      award_count:\n        description: badge award count\n        type: integer\n      earned_count:\n        description: badge earned count\n        type: integer\n      icon:\n        description: badge icon\n        type: string\n      id:\n        description: badge id\n        type: string\n      level:\n        allOf:\n        - $ref: '#/definitions/entity.BadgeLevel'\n        description: badge level\n      name:\n        description: badge name\n        type: string\n    type: object\n  schema.BadgeStatus:\n    enum:\n    - active\n    - inactive\n    type: string\n    x-enum-varnames:\n    - BadgeStatusActive\n    - BadgeStatusInactive\n  schema.CloseQuestionReq:\n    properties:\n      close_msg:\n        description: close_type\n        type: string\n      close_type:\n        description: close_type\n        type: integer\n      id:\n        type: string\n    required:\n    - id\n    type: object\n  schema.CollectionSwitchReq:\n    properties:\n      bookmark:\n        type: boolean\n      group_id:\n        type: string\n      object_id:\n        type: string\n    required:\n    - group_id\n    - object_id\n    type: object\n  schema.CollectionSwitchResp:\n    properties:\n      object_collection_count:\n        type: integer\n    type: object\n  schema.ConfigField:\n    properties:\n      description:\n        type: string\n      name:\n        type: string\n      options:\n        items:\n          $ref: '#/definitions/schema.ConfigFieldOption'\n        type: array\n      required:\n        type: boolean\n      title:\n        type: string\n      type:\n        type: string\n      ui_options:\n        $ref: '#/definitions/schema.ConfigFieldUIOptions'\n      value: {}\n    type: object\n  schema.ConfigFieldOption:\n    properties:\n      label:\n        type: string\n      value:\n        type: string\n    type: object\n  schema.ConfigFieldUIOptions:\n    properties:\n      action:\n        $ref: '#/definitions/schema.UIOptionAction'\n      class_name:\n        type: string\n      field_class_name:\n        type: string\n      input_type:\n        type: string\n      label:\n        type: string\n      placeholder:\n        type: string\n      rows:\n        type: string\n      text:\n        type: string\n      variant:\n        type: string\n    type: object\n  schema.ConnectorInfoResp:\n    properties:\n      icon:\n        type: string\n      link:\n        type: string\n      name:\n        type: string\n    type: object\n  schema.ConnectorUserInfoResp:\n    properties:\n      binding:\n        type: boolean\n      external_id:\n        type: string\n      icon:\n        type: string\n      link:\n        type: string\n      name:\n        type: string\n    type: object\n  schema.DeleteAPIKeyReq:\n    properties:\n      id:\n        type: integer\n    type: object\n  schema.DeletePermanentlyReq:\n    properties:\n      type:\n        enum:\n        - users\n        - questions\n        - answers\n        type: string\n    required:\n    - type\n    type: object\n  schema.EditUserProfileReq:\n    properties:\n      display_name:\n        maxLength: 30\n        minLength: 2\n        type: string\n      email:\n        maxLength: 500\n        type: string\n      user_id:\n        type: string\n      username:\n        maxLength: 30\n        minLength: 2\n        type: string\n    required:\n    - display_name\n    - email\n    - user_id\n    type: object\n  schema.ExternalLoginBindingUserSendEmailReq:\n    properties:\n      binding_key:\n        maxLength: 100\n        type: string\n      email:\n        maxLength: 512\n        type: string\n      must:\n        description: |-\n          If must is true, whatever email if exists, try to bind user.\n          If must is false, when email exist, will only be prompted with a warning.\n        type: boolean\n    required:\n    - binding_key\n    - email\n    type: object\n  schema.ExternalLoginBindingUserSendEmailResp:\n    properties:\n      access_token:\n        type: string\n      email_exist_and_must_be_confirmed:\n        type: boolean\n    type: object\n  schema.ExternalLoginUnbindingReq:\n    properties:\n      external_id:\n        maxLength: 128\n        type: string\n    required:\n    - external_id\n    type: object\n  schema.FollowReq:\n    properties:\n      is_cancel:\n        description: is cancel\n        type: boolean\n      object_id:\n        description: object id\n        type: string\n    required:\n    - object_id\n    type: object\n  schema.FollowResp:\n    properties:\n      follows:\n        description: the followers of object\n        type: integer\n      is_followed:\n        description: if user is followed object will be true,otherwise false\n        type: boolean\n    type: object\n  schema.GetAIModelResp:\n    properties:\n      created:\n        type: integer\n      id:\n        type: string\n      object:\n        type: string\n      owned_by:\n        type: string\n    type: object\n  schema.GetAIProviderResp:\n    properties:\n      default_api_host:\n        type: string\n      display_name:\n        type: string\n      name:\n        type: string\n    type: object\n  schema.GetAPIKeyResp:\n    properties:\n      access_key:\n        type: string\n      created_at:\n        type: integer\n      description:\n        type: string\n      id:\n        type: integer\n      last_used_at:\n        type: integer\n      scope:\n        type: string\n    type: object\n  schema.GetAnswerInfoResp:\n    properties:\n      info:\n        $ref: '#/definitions/schema.AnswerInfo'\n      question:\n        $ref: '#/definitions/schema.QuestionInfoResp'\n    type: object\n  schema.GetBadgeInfoResp:\n    properties:\n      award_count:\n        description: badge award count\n        type: integer\n      description:\n        description: badge description\n        type: string\n      earned_count:\n        description: badge earned count\n        type: integer\n      icon:\n        description: badge icon\n        type: string\n      id:\n        description: badge id\n        type: string\n      is_single:\n        description: badge is single or multiple\n        type: boolean\n      level:\n        allOf:\n        - $ref: '#/definitions/entity.BadgeLevel'\n        description: badge level\n      name:\n        description: badge name\n        type: string\n    type: object\n  schema.GetBadgeListPagedResp:\n    properties:\n      award_count:\n        description: badge award count\n        type: integer\n      description:\n        description: badge description\n        type: string\n      earned:\n        description: badge earned count\n        type: boolean\n      group_name:\n        description: badge group name\n        type: string\n      icon:\n        description: badge icon\n        type: string\n      id:\n        description: badge id\n        type: string\n      level:\n        allOf:\n        - $ref: '#/definitions/entity.BadgeLevel'\n        description: badge level\n      name:\n        description: badge name\n        type: string\n      status:\n        allOf:\n        - $ref: '#/definitions/schema.BadgeStatus'\n        description: badge status\n    type: object\n  schema.GetBadgeListResp:\n    properties:\n      badges:\n        description: badge list info\n        items:\n          $ref: '#/definitions/schema.BadgeListInfo'\n        type: array\n      group_name:\n        description: badge group name\n        type: string\n    type: object\n  schema.GetCommentPersonalWithPageResp:\n    properties:\n      answer_id:\n        description: answer id\n        type: string\n      comment_id:\n        description: comment id\n        type: string\n      content:\n        description: content\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      object_id:\n        description: object id\n        type: string\n      object_type:\n        description: object type\n        enum:\n        - question\n        - answer\n        - tag\n        - comment\n        type: string\n      question_id:\n        description: question id\n        type: string\n      title:\n        description: title\n        type: string\n      url_title:\n        description: url title\n        type: string\n    type: object\n  schema.GetCommentResp:\n    properties:\n      comment_id:\n        description: comment id\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      is_vote:\n        description: current user if already vote this comment\n        type: boolean\n      member_actions:\n        description: MemberActions\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      object_id:\n        description: object id\n        type: string\n      original_text:\n        description: original comment content\n        type: string\n      parsed_text:\n        description: parsed comment content\n        type: string\n      reply_comment_id:\n        description: reply comment id\n        type: string\n      reply_user_display_name:\n        description: reply user display name\n        type: string\n      reply_user_id:\n        description: reply user id\n        type: string\n      reply_user_status:\n        description: reply user status\n        type: string\n      reply_username:\n        description: reply user username\n        type: string\n      user_avatar:\n        description: user avatar\n        type: string\n      user_display_name:\n        description: user display name\n        type: string\n      user_id:\n        description: user id\n        type: string\n      user_status:\n        description: user status\n        type: string\n      username:\n        description: username\n        type: string\n      vote_count:\n        description: user vote amount\n        type: integer\n    type: object\n  schema.GetCurrentLoginUserInfoResp:\n    properties:\n      access_token:\n        description: access token\n        type: string\n      answer_count:\n        description: answer count\n        type: integer\n      authority_group:\n        description: authority group\n        type: integer\n      avatar:\n        $ref: '#/definitions/schema.AvatarInfo'\n      bio:\n        description: bio markdown\n        type: string\n      bio_html:\n        description: bio html\n        type: string\n      color_scheme:\n        description: Color scheme\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      display_name:\n        description: display name\n        type: string\n      e_mail:\n        description: email\n        type: string\n      follow_count:\n        description: follow count\n        type: integer\n      have_password:\n        description: user have password\n        type: boolean\n      id:\n        description: user id\n        type: string\n      language:\n        description: language\n        type: string\n      last_login_date:\n        description: last login date\n        type: integer\n      location:\n        description: location\n        type: string\n      mail_status:\n        description: mail status(1 pass 2 to be verified)\n        type: integer\n      mobile:\n        description: mobile\n        type: string\n      notice_status:\n        description: notice status(1 on 2off)\n        type: integer\n      question_count:\n        description: question count\n        type: integer\n      rank:\n        description: rank\n        type: integer\n      role_id:\n        description: role id\n        type: integer\n      status:\n        description: user status\n        type: string\n      suspended_until:\n        description: suspended until timestamp\n        type: integer\n      username:\n        description: username\n        type: string\n      visit_token:\n        description: visit token\n        type: string\n      website:\n        description: website\n        type: string\n    type: object\n  schema.GetFollowingTagsResp:\n    properties:\n      display_name:\n        description: display name\n        type: string\n      main_tag_slug_name:\n        description: if main tag slug name is not empty, this tag is synonymous with\n          the main tag\n        type: string\n      recommend:\n        type: boolean\n      reserved:\n        type: boolean\n      slug_name:\n        description: slug name\n        type: string\n      tag_id:\n        description: tag id\n        type: string\n    type: object\n  schema.GetObjectTimelineResp:\n    properties:\n      object_info:\n        $ref: '#/definitions/schema.ActObjectInfo'\n      timeline:\n        items:\n          $ref: '#/definitions/schema.ActObjectTimeline'\n        type: array\n    type: object\n  schema.GetOtherUserInfoByUsernameResp:\n    properties:\n      answer_count:\n        description: answer count\n        type: integer\n      avatar:\n        description: avatar\n        type: string\n      bio:\n        description: bio markdown\n        type: string\n      bio_html:\n        description: bio html\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      display_name:\n        description: display name\n        type: string\n      follow_count:\n        description: |-\n          email\n          follow count\n        type: integer\n      id:\n        description: user id\n        type: string\n      last_login_date:\n        description: last login date\n        type: integer\n      location:\n        description: location\n        type: string\n      mobile:\n        description: mobile\n        type: string\n      question_count:\n        description: question count\n        type: integer\n      rank:\n        description: rank\n        type: integer\n      status:\n        type: string\n      status_msg:\n        type: string\n      suspended_until:\n        description: suspended until timestamp\n        type: integer\n      username:\n        description: username\n        type: string\n      website:\n        description: website\n        type: string\n    type: object\n  schema.GetOtherUserInfoResp:\n    properties:\n      info:\n        $ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'\n    type: object\n  schema.GetPluginConfigResp:\n    properties:\n      config_fields:\n        items:\n          $ref: '#/definitions/schema.ConfigField'\n        type: array\n      description:\n        type: string\n      name:\n        type: string\n      slug_name:\n        type: string\n      version:\n        type: string\n    type: object\n  schema.GetPluginListResp:\n    properties:\n      description:\n        type: string\n      enabled:\n        type: boolean\n      have_config:\n        type: boolean\n      link:\n        type: string\n      name:\n        type: string\n      slug_name:\n        type: string\n      version:\n        type: string\n    type: object\n  schema.GetPrivilegesConfigResp:\n    properties:\n      options:\n        items:\n          $ref: '#/definitions/schema.PrivilegeOption'\n        type: array\n      selected_level:\n        $ref: '#/definitions/schema.PrivilegeLevel'\n    type: object\n  schema.GetRankPersonalPageResp:\n    properties:\n      answer_id:\n        description: answer id\n        type: string\n      content:\n        description: content\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      object_id:\n        description: object id\n        type: string\n      object_type:\n        description: object type\n        enum:\n        - question\n        - answer\n        - tag\n        - comment\n        type: string\n      question_id:\n        description: question id\n        type: string\n      rank_type:\n        description: rank type\n        type: string\n      reputation:\n        description: reputation\n        type: integer\n      title:\n        description: title\n        type: string\n      url_title:\n        description: url title\n        type: string\n    type: object\n  schema.GetReportListPageResp:\n    properties:\n      answer_accepted:\n        type: boolean\n      answer_count:\n        type: integer\n      answer_id:\n        type: string\n      author_user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      comment_id:\n        type: string\n      created_at:\n        type: integer\n      flag_id:\n        type: string\n      object_id:\n        type: string\n      object_show_status:\n        type: integer\n      object_status:\n        type: integer\n      object_type:\n        enum:\n        - question\n        - answer\n        - comment\n        type: string\n      original_text:\n        type: string\n      parsed_text:\n        type: string\n      question_id:\n        type: string\n      reason:\n        $ref: '#/definitions/schema.ReasonItem'\n      reason_content:\n        type: string\n      submit_at:\n        type: integer\n      submitter_user:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      url_title:\n        type: string\n    type: object\n  schema.GetReviewingTypeResp:\n    properties:\n      label:\n        type: string\n      name:\n        type: string\n      todo_amount:\n        type: integer\n    type: object\n  schema.GetRevisionResp:\n    properties:\n      content: {}\n      create_at:\n        type: integer\n      id:\n        type: string\n      object_id:\n        type: string\n      reason:\n        type: string\n      status:\n        type: integer\n      title:\n        type: string\n      url_title:\n        type: string\n      use_id:\n        type: string\n      user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n    type: object\n  schema.GetRoleResp:\n    properties:\n      description:\n        type: string\n      id:\n        type: integer\n      name:\n        type: string\n    type: object\n  schema.GetSMTPConfigResp:\n    properties:\n      encryption:\n        description: '\"\" SSL TLS'\n        type: string\n      from_email:\n        type: string\n      from_name:\n        type: string\n      smtp_authentication:\n        type: boolean\n      smtp_host:\n        type: string\n      smtp_password:\n        type: string\n      smtp_port:\n        type: integer\n      smtp_username:\n        type: string\n    type: object\n  schema.GetSiteLegalInfoResp:\n    properties:\n      privacy_policy_original_text:\n        type: string\n      privacy_policy_parsed_text:\n        type: string\n      terms_of_service_original_text:\n        type: string\n      terms_of_service_parsed_text:\n        type: string\n    type: object\n  schema.GetTagBasicResp:\n    properties:\n      display_name:\n        type: string\n      recommend:\n        type: boolean\n      reserved:\n        type: boolean\n      slug_name:\n        type: string\n      tag_id:\n        type: string\n    type: object\n  schema.GetTagPageResp:\n    properties:\n      created_at:\n        description: created time\n        type: integer\n      description:\n        description: description\n        type: string\n      display_name:\n        description: display_name\n        type: string\n      excerpt:\n        description: excerpt\n        type: string\n      follow_count:\n        description: follower amount\n        type: integer\n      is_follower:\n        description: is follower\n        type: boolean\n      original_text:\n        description: original text\n        type: string\n      parsed_text:\n        description: parsed_text\n        type: string\n      question_count:\n        description: question amount\n        type: integer\n      recommend:\n        type: boolean\n      reserved:\n        type: boolean\n      slug_name:\n        description: slug_name\n        type: string\n      tag_id:\n        description: tag_id\n        type: string\n      updated_at:\n        description: updated time\n        type: integer\n    type: object\n  schema.GetTagResp:\n    properties:\n      created_at:\n        type: integer\n      description:\n        type: string\n      display_name:\n        type: string\n      excerpt:\n        type: string\n      follow_count:\n        type: integer\n      is_follower:\n        type: boolean\n      main_tag_slug_name:\n        description: if main tag slug name is not empty, this tag is synonymous with\n          the main tag\n        type: string\n      member_actions:\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      original_text:\n        type: string\n      parsed_text:\n        type: string\n      question_count:\n        type: integer\n      recommend:\n        type: boolean\n      reserved:\n        type: boolean\n      slug_name:\n        type: string\n      status:\n        type: string\n      tag_id:\n        type: string\n      updated_at:\n        type: integer\n    type: object\n  schema.GetTagSynonymsResp:\n    properties:\n      member_actions:\n        description: MemberActions\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      synonyms:\n        description: synonyms\n        items:\n          $ref: '#/definitions/schema.TagSynonym'\n        type: array\n    type: object\n  schema.GetUnreviewedPostPageResp:\n    properties:\n      answer_id:\n        type: string\n      author_user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      comment_id:\n        type: string\n      created_at:\n        type: integer\n      object_id:\n        type: string\n      object_show_status:\n        type: integer\n      object_status:\n        type: integer\n      object_type:\n        enum:\n        - question\n        - answer\n        - comment\n        type: string\n      original_text:\n        type: string\n      parsed_text:\n        type: string\n      question_id:\n        type: string\n      reason:\n        type: string\n      review_id:\n        type: integer\n      submit_at:\n        type: integer\n      submitter_display_name:\n        type: string\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      url_title:\n        type: string\n    type: object\n  schema.GetUnreviewedRevisionResp:\n    properties:\n      info:\n        $ref: '#/definitions/schema.UnreviewedRevisionInfoInfo'\n      type:\n        type: string\n      unreviewed_info:\n        $ref: '#/definitions/schema.GetRevisionResp'\n    type: object\n  schema.GetUserActivationResp:\n    properties:\n      activation_url:\n        type: string\n    type: object\n  schema.GetUserBadgeAwardListResp:\n    properties:\n      earned_count:\n        description: badge award count\n        type: integer\n      icon:\n        description: badge icon\n        type: string\n      id:\n        description: badge id\n        type: string\n      level:\n        allOf:\n        - $ref: '#/definitions/entity.BadgeLevel'\n        description: badge level\n      name:\n        description: badge name\n        type: string\n    type: object\n  schema.GetUserNotificationConfigResp:\n    properties:\n      all_new_question:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n      all_new_question_for_following_tags:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n      inbox:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n    type: object\n  schema.GetUserPageResp:\n    properties:\n      avatar:\n        description: avatar\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      deleted_at:\n        description: delete time\n        type: integer\n      display_name:\n        description: display name\n        type: string\n      e_mail:\n        description: email\n        type: string\n      rank:\n        description: rank\n        type: integer\n      role_id:\n        description: role id\n        type: integer\n      role_name:\n        description: role name\n        type: string\n      status:\n        description: user status(normal,suspended,deleted,inactive)\n        type: string\n      suspended_at:\n        description: suspended time\n        type: integer\n      suspended_until:\n        description: suspended until time\n        type: integer\n      user_id:\n        description: user id\n        type: string\n      username:\n        description: username\n        type: string\n    type: object\n  schema.GetUserPluginListResp:\n    properties:\n      name:\n        type: string\n      slug_name:\n        type: string\n    type: object\n  schema.GetUserStaffResp:\n    properties:\n      avatar:\n        description: avatar\n        type: string\n      display_name:\n        description: display name\n        type: string\n      username:\n        description: username\n        type: string\n    type: object\n  schema.GetVoteWithPageResp:\n    properties:\n      answer_id:\n        description: answer id\n        type: string\n      content:\n        description: content\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      object_id:\n        description: object id\n        type: string\n      object_type:\n        description: object type\n        enum:\n        - question\n        - answer\n        - tag\n        - comment\n        type: string\n      question_id:\n        description: question id\n        type: string\n      title:\n        description: title\n        type: string\n      url_title:\n        description: url title\n        type: string\n      vote_type:\n        description: vote type\n        type: string\n    type: object\n  schema.LoadingAction:\n    properties:\n      state:\n        type: string\n      text:\n        type: string\n    type: object\n  schema.NotificationChannelConfig:\n    properties:\n      enable:\n        type: boolean\n      key:\n        $ref: '#/definitions/constant.NotificationChannelKey'\n    type: object\n  schema.NotificationClearIDRequest:\n    properties:\n      id:\n        type: string\n    type: object\n  schema.NotificationClearRequest:\n    properties:\n      type:\n        enum:\n        - inbox\n        - achievement\n        type: string\n    required:\n    - type\n    type: object\n  schema.OnCompleteAction:\n    properties:\n      refresh_form_config:\n        type: boolean\n      toast_return_message:\n        type: boolean\n    type: object\n  schema.Operation:\n    properties:\n      description:\n        type: string\n      level:\n        $ref: '#/definitions/schema.OperationLevel'\n      msg:\n        type: string\n      time:\n        type: integer\n      type:\n        type: string\n    type: object\n  schema.OperationLevel:\n    enum:\n    - info\n    - danger\n    - warning\n    - secondary\n    type: string\n    x-enum-varnames:\n    - OperationLevelInfo\n    - OperationLevelDanger\n    - OperationLevelWarning\n    - OperationLevelSecondary\n  schema.OperationQuestionReq:\n    properties:\n      id:\n        type: string\n      operation:\n        description: operation [pin unpin hide show]\n        type: string\n    required:\n    - id\n    type: object\n  schema.PermissionMemberAction:\n    properties:\n      action:\n        type: string\n      name:\n        type: string\n      type:\n        type: string\n    type: object\n  schema.PostRenderReq:\n    properties:\n      content:\n        type: string\n    type: object\n  schema.PrivilegeLevel:\n    enum:\n    - 1\n    - 2\n    - 3\n    - 99\n    type: integer\n    x-enum-varnames:\n    - PrivilegeLevel1\n    - PrivilegeLevel2\n    - PrivilegeLevel3\n    - PrivilegeLevelCustom\n  schema.PrivilegeOption:\n    properties:\n      level:\n        $ref: '#/definitions/schema.PrivilegeLevel'\n      level_desc:\n        type: string\n      privileges:\n        items:\n          $ref: '#/definitions/constant.Privilege'\n        type: array\n    type: object\n  schema.QuestionAdd:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      content:\n        description: content\n        maxLength: 65535\n        minLength: 0\n        type: string\n      tags:\n        description: tags\n        items:\n          $ref: '#/definitions/schema.TagItem'\n        type: array\n      title:\n        description: question title\n        maxLength: 150\n        minLength: 6\n        type: string\n    required:\n    - title\n    type: object\n  schema.QuestionAddByAnswer:\n    properties:\n      answer_content:\n        maxLength: 65535\n        minLength: 6\n        type: string\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      content:\n        description: content\n        maxLength: 65535\n        minLength: 0\n        type: string\n      mention_username_list:\n        items:\n          type: string\n        type: array\n      tags:\n        description: tags\n        items:\n          $ref: '#/definitions/schema.TagItem'\n        type: array\n      title:\n        description: question title\n        maxLength: 150\n        minLength: 6\n        type: string\n    required:\n    - answer_content\n    - title\n    type: object\n  schema.QuestionInfoResp:\n    properties:\n      accepted_answer_id:\n        type: string\n      answer_count:\n        type: integer\n      answered:\n        type: boolean\n      collected:\n        type: boolean\n      collection_count:\n        type: integer\n      content:\n        type: string\n      create_time:\n        type: integer\n      description:\n        type: string\n      edit_time:\n        type: integer\n      extends_actions:\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      first_answer_id:\n        type: string\n      follow_count:\n        type: integer\n      html:\n        type: string\n      id:\n        type: string\n      is_followed:\n        type: boolean\n      last_answer_id:\n        type: string\n      last_answered_user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      member_actions:\n        description: MemberActions\n        items:\n          $ref: '#/definitions/schema.PermissionMemberAction'\n        type: array\n      operation:\n        $ref: '#/definitions/schema.Operation'\n      pin:\n        type: integer\n      show:\n        type: integer\n      status:\n        type: integer\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      unique_view_count:\n        type: integer\n      update_time:\n        type: integer\n      update_user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      url_title:\n        type: string\n      user_info:\n        $ref: '#/definitions/schema.UserBasicInfo'\n      view_count:\n        type: integer\n      vote_count:\n        type: integer\n      vote_status:\n        type: string\n    type: object\n  schema.QuestionPageReq:\n    properties:\n      in_days:\n        minimum: 1\n        type: integer\n      order:\n        enum:\n        - newest\n        - active\n        - hot\n        - score\n        - unanswered\n        - recommend\n        - frequent\n        type: string\n      page:\n        minimum: 1\n        type: integer\n      page_size:\n        minimum: 1\n        type: integer\n      tag:\n        maxLength: 100\n        type: string\n      username:\n        maxLength: 100\n        type: string\n    type: object\n  schema.QuestionPageResp:\n    properties:\n      accepted_answer_id:\n        description: answer information\n        type: string\n      answer_count:\n        type: integer\n      collection_count:\n        type: integer\n      created_at:\n        type: integer\n      description:\n        type: string\n      follow_count:\n        type: integer\n      id:\n        type: string\n      last_answer_id:\n        type: string\n      operated_at:\n        description: operator information\n        type: integer\n      operation_type:\n        type: string\n      operator:\n        $ref: '#/definitions/schema.QuestionPageRespOperator'\n      pin:\n        description: '1: unpin, 2: pin'\n        type: integer\n      show:\n        description: '0: show, 1: hide'\n        type: integer\n      status:\n        type: integer\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      unique_view_count:\n        type: integer\n      url_title:\n        type: string\n      view_count:\n        description: question statistical information\n        type: integer\n      vote_count:\n        type: integer\n    type: object\n  schema.QuestionPageRespOperator:\n    properties:\n      avatar:\n        type: string\n      display_name:\n        type: string\n      id:\n        type: string\n      rank:\n        type: integer\n      status:\n        type: string\n      username:\n        type: string\n    type: object\n  schema.QuestionRecoverReq:\n    properties:\n      question_id:\n        type: string\n    required:\n    - question_id\n    type: object\n  schema.QuestionUpdate:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      content:\n        description: content\n        maxLength: 65535\n        minLength: 0\n        type: string\n      edit_summary:\n        description: edit summary\n        type: string\n      id:\n        description: question id\n        type: string\n      invite_user:\n        items:\n          type: string\n        type: array\n      tags:\n        description: tags\n        items:\n          $ref: '#/definitions/schema.TagItem'\n        type: array\n      title:\n        description: question title\n        maxLength: 150\n        minLength: 6\n        type: string\n    required:\n    - id\n    - title\n    type: object\n  schema.QuestionUpdateInviteUser:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      id:\n        type: string\n      invite_user:\n        items:\n          type: string\n        type: array\n    required:\n    - id\n    type: object\n  schema.ReactionRespItem:\n    properties:\n      count:\n        description: Count is the number of users who reacted\n        type: integer\n      emoji:\n        description: Emoji is the reaction emoji\n        type: string\n      is_active:\n        description: IsActive is if current user has reacted\n        type: boolean\n      tooltip:\n        description: Tooltip is the user's name who reacted\n        type: string\n    type: object\n  schema.ReasonItem:\n    properties:\n      content_type:\n        type: string\n      description:\n        type: string\n      name:\n        type: string\n      placeholder:\n        type: string\n      reason_key:\n        type: string\n      reason_type:\n        type: integer\n    type: object\n  schema.RecoverAnswerReq:\n    properties:\n      answer_id:\n        type: string\n    required:\n    - answer_id\n    type: object\n  schema.RecoverTagReq:\n    properties:\n      tag_id:\n        type: string\n    required:\n    - tag_id\n    type: object\n  schema.RemoveAnswerReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      id:\n        type: string\n    required:\n    - id\n    type: object\n  schema.RemoveCommentReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      comment_id:\n        description: comment id\n        type: string\n    required:\n    - comment_id\n    type: object\n  schema.RemoveQuestionReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: captcha_id\n        type: string\n      id:\n        description: question id\n        type: string\n    required:\n    - id\n    type: object\n  schema.RemoveTagReq:\n    properties:\n      tag_id:\n        description: tag_id\n        type: string\n    required:\n    - tag_id\n    type: object\n  schema.ReopenQuestionReq:\n    properties:\n      question_id:\n        type: string\n    type: object\n  schema.ReviewReportReq:\n    properties:\n      close_msg:\n        type: string\n      close_type:\n        type: integer\n      content:\n        maxLength: 65535\n        minLength: 6\n        type: string\n      flag_id:\n        type: string\n      operation_type:\n        enum:\n        - edit_post\n        - close_post\n        - delete_post\n        - unlist_post\n        - ignore_report\n        type: string\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagItem'\n        type: array\n      title:\n        maxLength: 150\n        minLength: 6\n        type: string\n    required:\n    - flag_id\n    - operation_type\n    type: object\n  schema.RevisionAuditReq:\n    properties:\n      id:\n        description: object id\n        type: string\n      operation:\n        description: approve or reject\n        type: string\n    required:\n    - id\n    - operation\n    type: object\n  schema.SearchObject:\n    properties:\n      accepted:\n        type: boolean\n      answer_count:\n        type: integer\n      created_at:\n        type: integer\n      excerpt:\n        type: string\n      id:\n        type: string\n      question_id:\n        type: string\n      status:\n        description: Status\n        type: string\n      tags:\n        description: tags\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      url_title:\n        type: string\n      user_info:\n        allOf:\n        - $ref: '#/definitions/schema.SearchObjectUser'\n        description: user info\n      vote_count:\n        type: integer\n    type: object\n  schema.SearchObjectUser:\n    properties:\n      display_name:\n        type: string\n      id:\n        type: string\n      rank:\n        type: integer\n      status:\n        type: string\n      username:\n        type: string\n    type: object\n  schema.SearchResp:\n    properties:\n      count:\n        type: integer\n      list:\n        description: search response\n        items:\n          $ref: '#/definitions/schema.SearchResult'\n        type: array\n    type: object\n  schema.SearchResult:\n    properties:\n      object:\n        allOf:\n        - $ref: '#/definitions/schema.SearchObject'\n        description: this object\n      object_type:\n        description: object_type\n        type: string\n    type: object\n  schema.SendUserActivationReq:\n    properties:\n      user_id:\n        type: string\n    required:\n    - user_id\n    type: object\n  schema.SiteAIProvider:\n    properties:\n      api_host:\n        maxLength: 512\n        type: string\n      api_key:\n        maxLength: 256\n        type: string\n      model:\n        maxLength: 100\n        type: string\n      provider:\n        maxLength: 50\n        type: string\n    type: object\n  schema.SiteAIReq:\n    properties:\n      ai_providers:\n        items:\n          $ref: '#/definitions/schema.SiteAIProvider'\n        type: array\n      chosen_provider:\n        maxLength: 50\n        type: string\n      enabled:\n        type: boolean\n      prompt_config:\n        $ref: '#/definitions/schema.AIPromptConfig'\n    type: object\n  schema.SiteAIResp:\n    properties:\n      ai_providers:\n        items:\n          $ref: '#/definitions/schema.SiteAIProvider'\n        type: array\n      chosen_provider:\n        maxLength: 50\n        type: string\n      enabled:\n        type: boolean\n      prompt_config:\n        $ref: '#/definitions/schema.AIPromptConfig'\n    type: object\n  schema.SiteAdvancedReq:\n    properties:\n      authorized_attachment_extensions:\n        items:\n          type: string\n        type: array\n      authorized_image_extensions:\n        items:\n          type: string\n        type: array\n      max_attachment_size:\n        type: integer\n      max_image_megapixel:\n        type: integer\n      max_image_size:\n        type: integer\n    type: object\n  schema.SiteAdvancedResp:\n    properties:\n      authorized_attachment_extensions:\n        items:\n          type: string\n        type: array\n      authorized_image_extensions:\n        items:\n          type: string\n        type: array\n      max_attachment_size:\n        type: integer\n      max_image_megapixel:\n        type: integer\n      max_image_size:\n        type: integer\n    type: object\n  schema.SiteBrandingReq:\n    properties:\n      favicon:\n        maxLength: 512\n        type: string\n      logo:\n        maxLength: 512\n        type: string\n      mobile_logo:\n        maxLength: 512\n        type: string\n      square_icon:\n        maxLength: 512\n        type: string\n    type: object\n  schema.SiteBrandingResp:\n    properties:\n      favicon:\n        maxLength: 512\n        type: string\n      logo:\n        maxLength: 512\n        type: string\n      mobile_logo:\n        maxLength: 512\n        type: string\n      square_icon:\n        maxLength: 512\n        type: string\n    type: object\n  schema.SiteCustomCssHTMLReq:\n    properties:\n      custom_css:\n        maxLength: 65536\n        type: string\n      custom_footer:\n        maxLength: 65536\n        type: string\n      custom_head:\n        maxLength: 65536\n        type: string\n      custom_header:\n        maxLength: 65536\n        type: string\n      custom_sidebar:\n        maxLength: 65536\n        type: string\n    type: object\n  schema.SiteCustomCssHTMLResp:\n    properties:\n      custom_css:\n        maxLength: 65536\n        type: string\n      custom_footer:\n        maxLength: 65536\n        type: string\n      custom_head:\n        maxLength: 65536\n        type: string\n      custom_header:\n        maxLength: 65536\n        type: string\n      custom_sidebar:\n        maxLength: 65536\n        type: string\n    type: object\n  schema.SiteGeneralReq:\n    properties:\n      contact_email:\n        maxLength: 512\n        type: string\n      description:\n        maxLength: 2000\n        type: string\n      name:\n        maxLength: 128\n        type: string\n      short_description:\n        maxLength: 255\n        type: string\n      site_url:\n        maxLength: 512\n        type: string\n    required:\n    - contact_email\n    - name\n    - site_url\n    type: object\n  schema.SiteGeneralResp:\n    properties:\n      contact_email:\n        maxLength: 512\n        type: string\n      description:\n        maxLength: 2000\n        type: string\n      name:\n        maxLength: 128\n        type: string\n      short_description:\n        maxLength: 255\n        type: string\n      site_url:\n        maxLength: 512\n        type: string\n    required:\n    - contact_email\n    - name\n    - site_url\n    type: object\n  schema.SiteInfoResp:\n    properties:\n      ai_enabled:\n        type: boolean\n      branding:\n        $ref: '#/definitions/schema.SiteBrandingResp'\n      custom_css_html:\n        $ref: '#/definitions/schema.SiteCustomCssHTMLResp'\n      general:\n        $ref: '#/definitions/schema.SiteGeneralResp'\n      interface:\n        $ref: '#/definitions/schema.SiteInterfaceSettingsResp'\n      login:\n        $ref: '#/definitions/schema.SiteLoginResp'\n      mcp_enabled:\n        type: boolean\n      revision:\n        type: string\n      site_advanced:\n        $ref: '#/definitions/schema.SiteAdvancedResp'\n      site_legal:\n        $ref: '#/definitions/schema.SiteLegalSimpleResp'\n      site_questions:\n        $ref: '#/definitions/schema.SiteQuestionsResp'\n      site_security:\n        $ref: '#/definitions/schema.SiteSecurityResp'\n      site_seo:\n        $ref: '#/definitions/schema.SiteSeoResp'\n      site_tags:\n        $ref: '#/definitions/schema.SiteTagsResp'\n      site_users:\n        $ref: '#/definitions/schema.SiteUsersResp'\n      theme:\n        $ref: '#/definitions/schema.SiteThemeResp'\n      users_settings:\n        $ref: '#/definitions/schema.SiteUsersSettingsResp'\n      version:\n        type: string\n    type: object\n  schema.SiteInterfaceReq:\n    properties:\n      language:\n        maxLength: 128\n        type: string\n      time_zone:\n        maxLength: 128\n        type: string\n    required:\n    - language\n    - time_zone\n    type: object\n  schema.SiteInterfaceSettingsResp:\n    properties:\n      language:\n        maxLength: 128\n        type: string\n      time_zone:\n        maxLength: 128\n        type: string\n    required:\n    - language\n    - time_zone\n    type: object\n  schema.SiteLegalSimpleResp:\n    properties:\n      external_content_display:\n        enum:\n        - always_display\n        - ask_before_display\n        type: string\n    required:\n    - external_content_display\n    type: object\n  schema.SiteLoginReq:\n    properties:\n      allow_email_domains:\n        items:\n          type: string\n        type: array\n      allow_email_registrations:\n        type: boolean\n      allow_new_registrations:\n        type: boolean\n      allow_password_login:\n        type: boolean\n    type: object\n  schema.SiteLoginResp:\n    properties:\n      allow_email_domains:\n        items:\n          type: string\n        type: array\n      allow_email_registrations:\n        type: boolean\n      allow_new_registrations:\n        type: boolean\n      allow_password_login:\n        type: boolean\n    type: object\n  schema.SiteMCPReq:\n    properties:\n      enabled:\n        type: boolean\n    type: object\n  schema.SiteMCPResp:\n    properties:\n      enabled:\n        type: boolean\n      http_header:\n        type: string\n      type:\n        type: string\n      url:\n        type: string\n    type: object\n  schema.SitePoliciesReq:\n    properties:\n      privacy_policy_original_text:\n        type: string\n      privacy_policy_parsed_text:\n        type: string\n      terms_of_service_original_text:\n        type: string\n      terms_of_service_parsed_text:\n        type: string\n    type: object\n  schema.SitePoliciesResp:\n    properties:\n      privacy_policy_original_text:\n        type: string\n      privacy_policy_parsed_text:\n        type: string\n      terms_of_service_original_text:\n        type: string\n      terms_of_service_parsed_text:\n        type: string\n    type: object\n  schema.SiteQuestionsReq:\n    properties:\n      min_content:\n        maximum: 65535\n        minimum: 0\n        type: integer\n      min_tags:\n        maximum: 5\n        minimum: 0\n        type: integer\n      restrict_answer:\n        type: boolean\n    type: object\n  schema.SiteQuestionsResp:\n    properties:\n      min_content:\n        maximum: 65535\n        minimum: 0\n        type: integer\n      min_tags:\n        maximum: 5\n        minimum: 0\n        type: integer\n      restrict_answer:\n        type: boolean\n    type: object\n  schema.SiteSecurityReq:\n    properties:\n      check_update:\n        type: boolean\n      external_content_display:\n        enum:\n        - always_display\n        - ask_before_display\n        type: string\n      login_required:\n        type: boolean\n    required:\n    - external_content_display\n    type: object\n  schema.SiteSecurityResp:\n    properties:\n      check_update:\n        type: boolean\n      external_content_display:\n        enum:\n        - always_display\n        - ask_before_display\n        type: string\n      login_required:\n        type: boolean\n    required:\n    - external_content_display\n    type: object\n  schema.SiteSeoReq:\n    properties:\n      permalink:\n        maximum: 4\n        minimum: 0\n        type: integer\n      robots:\n        type: string\n    required:\n    - permalink\n    - robots\n    type: object\n  schema.SiteSeoResp:\n    properties:\n      permalink:\n        maximum: 4\n        minimum: 0\n        type: integer\n      robots:\n        type: string\n    required:\n    - permalink\n    - robots\n    type: object\n  schema.SiteTagsReq:\n    properties:\n      recommend_tags:\n        items:\n          $ref: '#/definitions/schema.SiteWriteTag'\n        type: array\n      required_tag:\n        type: boolean\n      reserved_tags:\n        items:\n          $ref: '#/definitions/schema.SiteWriteTag'\n        type: array\n    type: object\n  schema.SiteTagsResp:\n    properties:\n      recommend_tags:\n        items:\n          $ref: '#/definitions/schema.SiteWriteTag'\n        type: array\n      required_tag:\n        type: boolean\n      reserved_tags:\n        items:\n          $ref: '#/definitions/schema.SiteWriteTag'\n        type: array\n    type: object\n  schema.SiteThemeReq:\n    properties:\n      color_scheme:\n        maxLength: 100\n        type: string\n      layout:\n        enum:\n        - Full-width\n        - Fixed-width\n        type: string\n      theme:\n        maxLength: 255\n        type: string\n      theme_config:\n        additionalProperties: {}\n        type: object\n    required:\n    - theme\n    type: object\n  schema.SiteThemeResp:\n    properties:\n      color_scheme:\n        type: string\n      layout:\n        type: string\n      theme:\n        type: string\n      theme_config:\n        additionalProperties: {}\n        type: object\n      theme_options:\n        items:\n          $ref: '#/definitions/schema.ThemeOption'\n        type: array\n    type: object\n  schema.SiteUsersReq:\n    properties:\n      allow_update_avatar:\n        type: boolean\n      allow_update_bio:\n        type: boolean\n      allow_update_display_name:\n        type: boolean\n      allow_update_location:\n        type: boolean\n      allow_update_username:\n        type: boolean\n      allow_update_website:\n        type: boolean\n      default_avatar:\n        enum:\n        - system\n        - gravatar\n        type: string\n      gravatar_base_url:\n        type: string\n    required:\n    - default_avatar\n    type: object\n  schema.SiteUsersResp:\n    properties:\n      allow_update_avatar:\n        type: boolean\n      allow_update_bio:\n        type: boolean\n      allow_update_display_name:\n        type: boolean\n      allow_update_location:\n        type: boolean\n      allow_update_username:\n        type: boolean\n      allow_update_website:\n        type: boolean\n      default_avatar:\n        enum:\n        - system\n        - gravatar\n        type: string\n      gravatar_base_url:\n        type: string\n    required:\n    - default_avatar\n    type: object\n  schema.SiteUsersSettingsReq:\n    properties:\n      default_avatar:\n        enum:\n        - system\n        - gravatar\n        type: string\n      gravatar_base_url:\n        type: string\n    required:\n    - default_avatar\n    type: object\n  schema.SiteUsersSettingsResp:\n    properties:\n      default_avatar:\n        enum:\n        - system\n        - gravatar\n        type: string\n      gravatar_base_url:\n        type: string\n    required:\n    - default_avatar\n    type: object\n  schema.SiteWriteTag:\n    properties:\n      display_name:\n        type: string\n      slug_name:\n        type: string\n    required:\n    - slug_name\n    type: object\n  schema.TagItem:\n    properties:\n      display_name:\n        description: display_name\n        maxLength: 35\n        type: string\n      original_text:\n        description: original text\n        type: string\n      slug_name:\n        description: slug_name\n        maxLength: 35\n        type: string\n    type: object\n  schema.TagResp:\n    properties:\n      display_name:\n        type: string\n      main_tag_slug_name:\n        description: if main tag slug name is not empty, this tag is synonymous with\n          the main tag\n        type: string\n      recommend:\n        type: boolean\n      reserved:\n        type: boolean\n      slug_name:\n        type: string\n    type: object\n  schema.TagSynonym:\n    properties:\n      display_name:\n        description: display name\n        type: string\n      main_tag_slug_name:\n        description: if main tag slug name is not empty, this tag is synonymous with\n          the main tag\n        type: string\n      slug_name:\n        description: slug name\n        type: string\n      tag_id:\n        description: tag id\n        type: string\n    type: object\n  schema.ThemeOption:\n    properties:\n      label:\n        type: string\n      value:\n        type: string\n    type: object\n  schema.UIOptionAction:\n    properties:\n      loading:\n        $ref: '#/definitions/schema.LoadingAction'\n      method:\n        type: string\n      on_complete:\n        $ref: '#/definitions/schema.OnCompleteAction'\n      url:\n        type: string\n    type: object\n  schema.UnreviewedRevisionInfoInfo:\n    properties:\n      answer_accepted:\n        type: boolean\n      answer_count:\n        type: integer\n      answer_id:\n        type: string\n      comment_id:\n        type: string\n      content:\n        type: string\n      created_at:\n        type: integer\n      html:\n        type: string\n      object_creator_user_id:\n        type: string\n      object_id:\n        type: string\n      object_type:\n        type: string\n      question_id:\n        type: string\n      show_status:\n        type: integer\n      status:\n        type: integer\n      tags:\n        items:\n          $ref: '#/definitions/schema.TagResp'\n        type: array\n      title:\n        type: string\n      url_title:\n        type: string\n    type: object\n  schema.UpdateAPIKeyReq:\n    properties:\n      description:\n        maxLength: 150\n        type: string\n      id:\n        type: integer\n    required:\n    - description\n    - id\n    type: object\n  schema.UpdateBadgeStatusReq:\n    properties:\n      id:\n        description: badge id\n        type: string\n      status:\n        allOf:\n        - $ref: '#/definitions/schema.BadgeStatus'\n        description: badge status\n    required:\n    - id\n    - status\n    type: object\n  schema.UpdateCommentReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        description: whether user can delete it\n        type: string\n      comment_id:\n        description: comment id\n        type: string\n      original_text:\n        description: original comment content\n        maxLength: 600\n        minLength: 2\n        type: string\n    required:\n    - comment_id\n    - original_text\n    type: object\n  schema.UpdateFollowTagsReq:\n    properties:\n      slug_name_list:\n        description: tag slug name list\n        items:\n          type: string\n        type: array\n    type: object\n  schema.UpdateInfoRequest:\n    properties:\n      avatar:\n        $ref: '#/definitions/schema.AvatarInfo'\n      bio:\n        maxLength: 4096\n        type: string\n      display_name:\n        maxLength: 30\n        minLength: 2\n        type: string\n      location:\n        maxLength: 100\n        type: string\n      username:\n        maxLength: 30\n        minLength: 2\n        type: string\n      website:\n        maxLength: 500\n        type: string\n    type: object\n  schema.UpdatePluginConfigReq:\n    properties:\n      config_fields:\n        additionalProperties: {}\n        type: object\n      plugin_slug_name:\n        maxLength: 100\n        type: string\n    required:\n    - plugin_slug_name\n    type: object\n  schema.UpdatePluginStatusReq:\n    properties:\n      enabled:\n        type: boolean\n      plugin_slug_name:\n        maxLength: 100\n        type: string\n    required:\n    - plugin_slug_name\n    type: object\n  schema.UpdatePrivilegesConfigReq:\n    properties:\n      custom_privileges:\n        items:\n          $ref: '#/definitions/constant.Privilege'\n        type: array\n      level:\n        allOf:\n        - $ref: '#/definitions/schema.PrivilegeLevel'\n        minimum: 1\n    required:\n    - level\n    type: object\n  schema.UpdateReactionReq:\n    properties:\n      emoji:\n        enum:\n        - heart\n        - smile\n        - frown\n        type: string\n      object_id:\n        type: string\n      reaction:\n        enum:\n        - activate\n        - deactivate\n        type: string\n    required:\n    - emoji\n    - object_id\n    - reaction\n    type: object\n  schema.UpdateReviewReq:\n    properties:\n      review_id:\n        type: integer\n      status:\n        enum:\n        - approve\n        - reject\n        type: string\n    required:\n    - review_id\n    - status\n    type: object\n  schema.UpdateSMTPConfigReq:\n    properties:\n      encryption:\n        description: '\"\" SSL TLS'\n        enum:\n        - SSL\n        - TLS\n        type: string\n      from_email:\n        maxLength: 256\n        type: string\n      from_name:\n        maxLength: 256\n        type: string\n      smtp_authentication:\n        type: boolean\n      smtp_host:\n        maxLength: 256\n        type: string\n      smtp_password:\n        maxLength: 256\n        type: string\n      smtp_port:\n        maximum: 65535\n        minimum: 1\n        type: integer\n      smtp_username:\n        maxLength: 256\n        type: string\n      test_email_recipient:\n        type: string\n    type: object\n  schema.UpdateTagReq:\n    properties:\n      display_name:\n        description: display_name\n        maxLength: 35\n        type: string\n      edit_summary:\n        description: edit summary\n        type: string\n      original_text:\n        description: original text\n        type: string\n      slug_name:\n        description: slug_name\n        maxLength: 35\n        type: string\n      tag_id:\n        description: tag_id\n        type: string\n    required:\n    - tag_id\n    type: object\n  schema.UpdateTagSynonymReq:\n    properties:\n      synonym_tag_list:\n        description: synonym tag list\n        items:\n          $ref: '#/definitions/schema.TagItem'\n        type: array\n      tag_id:\n        description: tag_id\n        type: string\n    required:\n    - synonym_tag_list\n    - tag_id\n    type: object\n  schema.UpdateUserInterfaceRequest:\n    properties:\n      color_scheme:\n        description: Color scheme\n        maxLength: 100\n        type: string\n      language:\n        description: language\n        maxLength: 100\n        type: string\n    required:\n    - color_scheme\n    - language\n    type: object\n  schema.UpdateUserNotificationConfigReq:\n    properties:\n      all_new_question:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n      all_new_question_for_following_tags:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n      inbox:\n        $ref: '#/definitions/schema.NotificationChannelConfig'\n    type: object\n  schema.UpdateUserPasswordReq:\n    properties:\n      password:\n        maxLength: 32\n        minLength: 8\n        type: string\n      user_id:\n        type: string\n    required:\n    - password\n    - user_id\n    type: object\n  schema.UpdateUserPluginConfigReq:\n    properties:\n      config_fields:\n        additionalProperties: {}\n        type: object\n      plugin_slug_name:\n        maxLength: 100\n        type: string\n    required:\n    - plugin_slug_name\n    type: object\n  schema.UpdateUserRoleReq:\n    properties:\n      role_id:\n        description: role id\n        type: integer\n      user_id:\n        description: user id\n        type: string\n    required:\n    - role_id\n    - user_id\n    type: object\n  schema.UpdateUserStatusReq:\n    properties:\n      remove_all_content:\n        type: boolean\n      status:\n        enum:\n        - normal\n        - suspended\n        - deleted\n        - inactive\n        type: string\n      suspend_duration:\n        enum:\n        - 24h\n        - 48h\n        - 72h\n        - 7d\n        - 14d\n        - 1m\n        - 2m\n        - 3m\n        - 6m\n        - 1y\n        - forever\n        type: string\n      user_id:\n        type: string\n    required:\n    - status\n    - user_id\n    type: object\n  schema.UserBasicInfo:\n    properties:\n      avatar:\n        type: string\n      display_name:\n        type: string\n      id:\n        type: string\n      language:\n        type: string\n      location:\n        type: string\n      rank:\n        type: integer\n      status:\n        type: string\n      suspended_until:\n        type: integer\n      username:\n        type: string\n      website:\n        type: string\n    type: object\n  schema.UserChangeEmailSendCodeReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      e_mail:\n        maxLength: 500\n        type: string\n      pass:\n        maxLength: 32\n        minLength: 8\n        type: string\n    required:\n    - e_mail\n    type: object\n  schema.UserChangeEmailVerifyReq:\n    properties:\n      code:\n        maxLength: 500\n        type: string\n    required:\n    - code\n    type: object\n  schema.UserEmailLoginReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      e_mail:\n        maxLength: 500\n        type: string\n      pass:\n        maxLength: 32\n        minLength: 8\n        type: string\n    required:\n    - e_mail\n    - pass\n    type: object\n  schema.UserLoginResp:\n    properties:\n      access_token:\n        description: access token\n        type: string\n      answer_count:\n        description: answer count\n        type: integer\n      authority_group:\n        description: authority group\n        type: integer\n      avatar:\n        description: avatar\n        type: string\n      bio:\n        description: bio markdown\n        type: string\n      bio_html:\n        description: bio html\n        type: string\n      color_scheme:\n        description: Color scheme\n        type: string\n      created_at:\n        description: create time\n        type: integer\n      display_name:\n        description: display name\n        type: string\n      e_mail:\n        description: email\n        type: string\n      follow_count:\n        description: follow count\n        type: integer\n      have_password:\n        description: user have password\n        type: boolean\n      id:\n        description: user id\n        type: string\n      language:\n        description: language\n        type: string\n      last_login_date:\n        description: last login date\n        type: integer\n      location:\n        description: location\n        type: string\n      mail_status:\n        description: mail status(1 pass 2 to be verified)\n        type: integer\n      mobile:\n        description: mobile\n        type: string\n      notice_status:\n        description: notice status(1 on 2off)\n        type: integer\n      question_count:\n        description: question count\n        type: integer\n      rank:\n        description: rank\n        type: integer\n      role_id:\n        description: role id\n        type: integer\n      status:\n        description: user status\n        type: string\n      suspended_until:\n        description: suspended until timestamp\n        type: integer\n      username:\n        description: username\n        type: string\n      visit_token:\n        description: visit token\n        type: string\n      website:\n        description: website\n        type: string\n    type: object\n  schema.UserModifyPasswordReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      old_pass:\n        maxLength: 32\n        minLength: 8\n        type: string\n      pass:\n        maxLength: 32\n        minLength: 8\n        type: string\n    required:\n    - pass\n    type: object\n  schema.UserRankingResp:\n    properties:\n      staffs:\n        items:\n          $ref: '#/definitions/schema.UserRankingSimpleInfo'\n        type: array\n      users_with_the_most_reputation:\n        items:\n          $ref: '#/definitions/schema.UserRankingSimpleInfo'\n        type: array\n      users_with_the_most_vote:\n        items:\n          $ref: '#/definitions/schema.UserRankingSimpleInfo'\n        type: array\n    type: object\n  schema.UserRankingSimpleInfo:\n    properties:\n      avatar:\n        description: avatar\n        type: string\n      display_name:\n        description: display name\n        type: string\n      rank:\n        description: rank\n        type: integer\n      username:\n        description: username\n        type: string\n      vote_count:\n        description: vote\n        type: integer\n    type: object\n  schema.UserRePassWordRequest:\n    properties:\n      code:\n        maxLength: 100\n        type: string\n      pass:\n        maxLength: 32\n        type: string\n    required:\n    - code\n    - pass\n    type: object\n  schema.UserRegisterReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      e_mail:\n        maxLength: 500\n        type: string\n      name:\n        maxLength: 30\n        minLength: 2\n        type: string\n      pass:\n        maxLength: 32\n        minLength: 8\n        type: string\n    required:\n    - e_mail\n    - name\n    - pass\n    type: object\n  schema.UserRetrievePassWordRequest:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      e_mail:\n        maxLength: 500\n        type: string\n    required:\n    - e_mail\n    type: object\n  schema.UserUnsubscribeNotificationReq:\n    properties:\n      code:\n        maxLength: 500\n        type: string\n    required:\n    - code\n    type: object\n  schema.VoteReq:\n    properties:\n      captcha_code:\n        type: string\n      captcha_id:\n        type: string\n      is_cancel:\n        type: boolean\n      object_id:\n        type: string\n    required:\n    - object_id\n    type: object\n  schema.VoteResp:\n    properties:\n      down_votes:\n        type: integer\n      up_votes:\n        type: integer\n      vote_status:\n        type: string\n      votes:\n        type: integer\n    type: object\n  translator.LangOption:\n    properties:\n      label:\n        type: string\n      progress:\n        description: Translation completion percentage\n        type: integer\n      value:\n        type: string\n    type: object\ninfo:\n  contact: {}\n  description: Apache Answer API\n  title: Apache Answer\npaths:\n  /:\n    get:\n      consumes:\n      - application/json\n      description: if config file not exist try to redirect to install page\n      produces:\n      - application/json\n      responses: {}\n      summary: if config file not exist try to redirect to install page\n      tags:\n      - installation\n  /answer/admin/api/ai-config:\n    get:\n      description: get AI configuration\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteAIResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get AI configuration\n      tags:\n      - admin\n    put:\n      description: update AI configuration\n      parameters:\n      - description: AI config\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteAIReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update AI configuration\n      tags:\n      - admin\n  /answer/admin/api/ai-models:\n    post:\n      description: get AI models\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetAIModelResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get AI models\n      tags:\n      - admin\n  /answer/admin/api/ai-provider:\n    get:\n      description: get AI provider configuration\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetAIProviderResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get AI provider configuration\n      tags:\n      - admin\n  /answer/admin/api/ai/conversation:\n    delete:\n      consumes:\n      - application/json\n      description: delete conversation and its related records for admin\n      parameters:\n      - description: apikey\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AIConversationAdminDeleteReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: delete conversation for admin\n      tags:\n      - ai-conversation-admin\n    get:\n      consumes:\n      - application/json\n      description: get conversation detail for admin\n      parameters:\n      - description: conversation id\n        in: query\n        name: conversation_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.AIConversationAdminDetailResp'\n              type: object\n      summary: get conversation detail for admin\n      tags:\n      - ai-conversation-admin\n  /answer/admin/api/ai/conversation/page:\n    get:\n      consumes:\n      - application/json\n      description: get conversation list for admin\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.AIConversationAdminListItem'\n                        type: array\n                    type: object\n              type: object\n      summary: get conversation list for admin\n      tags:\n      - ai-conversation-admin\n  /answer/admin/api/answer/page:\n    get:\n      consumes:\n      - application/json\n      description: Status:[available,deleted,pending]\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: user status\n        enum:\n        - available\n        - deleted\n        - pending\n        in: query\n        name: status\n        type: string\n      - description: answer id or question title\n        in: query\n        name: query\n        type: string\n      - description: question id\n        in: query\n        name: question_id\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: AdminAnswerPage admin answer page\n      tags:\n      - admin\n  /answer/admin/api/answer/status:\n    put:\n      consumes:\n      - application/json\n      description: update answer status\n      parameters:\n      - description: AdminUpdateAnswerStatusReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AdminUpdateAnswerStatusReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update answer status\n      tags:\n      - admin\n  /answer/admin/api/api-key:\n    delete:\n      description: delete apikey\n      parameters:\n      - description: apikey\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.DeleteAPIKeyReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: delete apikey\n      tags:\n      - admin\n    post:\n      description: add apikey\n      parameters:\n      - description: apikey\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddAPIKeyReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.AddAPIKeyResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: add apikey\n      tags:\n      - admin\n    put:\n      description: update apikey\n      parameters:\n      - description: apikey\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateAPIKeyReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update apikey\n      tags:\n      - admin\n  /answer/admin/api/api-key/all:\n    get:\n      description: get all api keys\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetAPIKeyResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get all api keys\n      tags:\n      - admin\n  /answer/admin/api/badge/status:\n    put:\n      consumes:\n      - application/json\n      description: update badge status\n      parameters:\n      - description: UpdateBadgeStatusReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateBadgeStatusReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update badge status\n      tags:\n      - AdminBadge\n  /answer/admin/api/badges:\n    get:\n      consumes:\n      - application/json\n      description: list all badges by page\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: badge status\n        enum:\n        - \"\"\n        - active\n        - inactive\n        in: query\n        name: status\n        type: string\n      - description: search param\n        in: query\n        name: q\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetBadgeListPagedResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: list all badges by page\n      tags:\n      - AdminBadge\n  /answer/admin/api/dashboard:\n    get:\n      consumes:\n      - application/json\n      description: DashboardInfo\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: DashboardInfo\n      tags:\n      - admin\n  /answer/admin/api/delete/permanently:\n    delete:\n      consumes:\n      - application/json\n      description: delete permanently\n      parameters:\n      - description: DeletePermanentlyReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.DeletePermanentlyReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: delete permanently\n      tags:\n      - admin\n  /answer/admin/api/language/options:\n    get:\n      description: Get language options\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Get language options\n      tags:\n      - Lang\n  /answer/admin/api/mcp-config:\n    get:\n      description: get MCP configuration\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteMCPResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get MCP configuration\n      tags:\n      - admin\n    put:\n      description: update MCP configuration\n      parameters:\n      - description: MCP config\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteMCPReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update MCP configuration\n      tags:\n      - admin\n  /answer/admin/api/plugin/config:\n    get:\n      description: get plugin config\n      parameters:\n      - description: plugin_slug_name\n        in: query\n        name: plugin_slug_name\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetPluginConfigResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get plugin config\n      tags:\n      - AdminPlugin\n    put:\n      consumes:\n      - application/json\n      description: update plugin config\n      parameters:\n      - description: UpdatePluginConfigReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdatePluginConfigReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update plugin config\n      tags:\n      - AdminPlugin\n  /answer/admin/api/plugin/status:\n    put:\n      consumes:\n      - application/json\n      description: update plugin status\n      parameters:\n      - description: UpdatePluginStatusReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdatePluginStatusReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update plugin status\n      tags:\n      - AdminPlugin\n  /answer/admin/api/plugins:\n    get:\n      consumes:\n      - application/json\n      description: get plugin list\n      parameters:\n      - description: 'status: active/inactive'\n        in: query\n        name: status\n        type: string\n      - description: have config\n        in: query\n        name: have_config\n        type: boolean\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetPluginListResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get plugin list\n      tags:\n      - AdminPlugin\n  /answer/admin/api/question/page:\n    get:\n      consumes:\n      - application/json\n      description: Status:[available,closed,deleted,pending]\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: user status\n        enum:\n        - available\n        - closed\n        - deleted\n        - pending\n        in: query\n        name: status\n        type: string\n      - description: question id or title\n        in: query\n        name: query\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: AdminQuestionPage admin question page\n      tags:\n      - admin\n  /answer/admin/api/question/status:\n    put:\n      consumes:\n      - application/json\n      description: update question status\n      parameters:\n      - description: AdminUpdateQuestionStatusReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AdminUpdateQuestionStatusReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update question status\n      tags:\n      - admin\n  /answer/admin/api/reasons:\n    get:\n      consumes:\n      - application/json\n      description: get reasons by object type and action\n      parameters:\n      - description: object_type\n        enum:\n        - question\n        - answer\n        - comment\n        - user\n        in: query\n        name: object_type\n        required: true\n        type: string\n      - description: action\n        enum:\n        - status\n        - close\n        - flag\n        - review\n        in: query\n        name: action\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: get reasons by object type and action\n      tags:\n      - reason\n  /answer/admin/api/roles:\n    get:\n      description: get role list\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetRoleResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get role list\n      tags:\n      - admin\n  /answer/admin/api/setting/privileges:\n    get:\n      description: GetPrivilegesConfig get privileges config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetPrivilegesConfigResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: GetPrivilegesConfig get privileges config\n      tags:\n      - admin\n    put:\n      description: update privileges config\n      parameters:\n      - description: config\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdatePrivilegesConfigReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update privileges config\n      tags:\n      - admin\n  /answer/admin/api/setting/smtp:\n    get:\n      description: GetSMTPConfig get smtp config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetSMTPConfigResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: GetSMTPConfig get smtp config\n      tags:\n      - admin\n    put:\n      description: update smtp config\n      parameters:\n      - description: smtp config\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateSMTPConfigReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update smtp config\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/advanced:\n    get:\n      description: get site advanced setting\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteAdvancedResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site advanced setting\n      tags:\n      - admin\n    put:\n      description: update site advanced info\n      parameters:\n      - description: advanced settings\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteAdvancedReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site advanced info\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/branding:\n    get:\n      description: get site interface\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteBrandingResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site interface\n      tags:\n      - admin\n    put:\n      description: update site info branding\n      parameters:\n      - description: branding info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteBrandingReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site info branding\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/custom-css-html:\n    get:\n      description: get site info custom html css config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteCustomCssHTMLResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site info custom html css config\n      tags:\n      - admin\n    put:\n      description: update site custom css html config\n      parameters:\n      - description: login info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteCustomCssHTMLReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site custom css html config\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/general:\n    get:\n      description: get site general information\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteGeneralResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site general information\n      tags:\n      - admin\n    put:\n      description: update site general information\n      parameters:\n      - description: general\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteGeneralReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site general information\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/interface:\n    get:\n      description: get site interface\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteInterfaceSettingsResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site interface\n      tags:\n      - admin\n    put:\n      description: update site info interface\n      parameters:\n      - description: general\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteInterfaceReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site info interface\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/login:\n    get:\n      description: get site info login config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteLoginResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site info login config\n      tags:\n      - admin\n    put:\n      description: update site login\n      parameters:\n      - description: login info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteLoginReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site login\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/polices:\n    get:\n      description: Get the policies information for the site\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SitePoliciesResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: Get the policies information for the site\n      tags:\n      - admin\n    put:\n      description: update site policies configuration\n      parameters:\n      - description: write info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SitePoliciesReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site policies configuration\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/question:\n    get:\n      description: get site questions setting\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteQuestionsResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site questions setting\n      tags:\n      - admin\n    put:\n      description: update site question settings\n      parameters:\n      - description: questions settings\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteQuestionsReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site question settings\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/security:\n    get:\n      description: Get the security information for the site\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteSecurityResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: Get the security information for the site\n      tags:\n      - admin\n    put:\n      description: update site security configuration\n      parameters:\n      - description: write info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteSecurityReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site security configuration\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/seo:\n    get:\n      description: get site seo information\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteSeoResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site seo information\n      tags:\n      - admin\n    put:\n      description: update site seo information\n      parameters:\n      - description: seo\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteSeoReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site seo information\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/tag:\n    get:\n      description: get site tags setting\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteTagsResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site tags setting\n      tags:\n      - admin\n    put:\n      description: update site tag settings\n      parameters:\n      - description: tags settings\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteTagsReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site tag settings\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/theme:\n    get:\n      description: get site info theme config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteThemeResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site info theme config\n      tags:\n      - admin\n    put:\n      description: update site custom css html config\n      parameters:\n      - description: login info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteThemeReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site custom css html config\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/users:\n    get:\n      description: get site user config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteUsersResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site user config\n      tags:\n      - admin\n    put:\n      description: update site info config about users\n      parameters:\n      - description: users info\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteUsersReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site info config about users\n      tags:\n      - admin\n  /answer/admin/api/siteinfo/users-settings:\n    get:\n      description: get site interface\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteUsersSettingsResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get site interface\n      tags:\n      - admin\n    put:\n      description: update site info users settings\n      parameters:\n      - description: general\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SiteUsersSettingsReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update site info users settings\n      tags:\n      - admin\n  /answer/admin/api/theme/options:\n    get:\n      description: Get theme options\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Get theme options\n      tags:\n      - admin\n  /answer/admin/api/user:\n    post:\n      consumes:\n      - application/json\n      description: add user\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddUserReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add user\n      tags:\n      - admin\n  /answer/admin/api/user/activation:\n    get:\n      description: get user activation\n      parameters:\n      - description: user id\n        in: query\n        name: user_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetUserActivationResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get user activation\n      tags:\n      - admin\n  /answer/admin/api/user/password:\n    put:\n      consumes:\n      - application/json\n      description: update user password\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserPasswordReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user password\n      tags:\n      - admin\n  /answer/admin/api/user/profile:\n    put:\n      consumes:\n      - application/json\n      description: edit user profile\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.EditUserProfileReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: edit user profile\n      tags:\n      - admin\n  /answer/admin/api/user/role:\n    put:\n      consumes:\n      - application/json\n      description: update user role\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserRoleReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user role\n      tags:\n      - admin\n  /answer/admin/api/user/status:\n    put:\n      consumes:\n      - application/json\n      description: update user\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserStatusReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user\n      tags:\n      - admin\n  /answer/admin/api/users:\n    post:\n      consumes:\n      - application/json\n      description: add users\n      parameters:\n      - description: user\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddUsersReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add users\n      tags:\n      - admin\n  /answer/admin/api/users/activation:\n    post:\n      description: send user activation\n      parameters:\n      - description: SendUserActivationReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.SendUserActivationReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: send user activation\n      tags:\n      - admin\n  /answer/admin/api/users/page:\n    get:\n      description: get user page\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: 'search query: email, username or id:[id]'\n        in: query\n        name: query\n        type: string\n      - description: staff user\n        in: query\n        name: staff\n        type: boolean\n      - description: user status\n        enum:\n        - suspended\n        - deleted\n        - inactive\n        in: query\n        name: status\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      records:\n                        items:\n                          $ref: '#/definitions/schema.GetUserPageResp'\n                        type: array\n                    type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get user page\n      tags:\n      - admin\n  /answer/api/v1/activity/timeline:\n    get:\n      description: get object timeline\n      parameters:\n      - description: object id\n        in: query\n        name: object_id\n        type: string\n      - description: tag slug name\n        in: query\n        name: tag_slug_name\n        type: string\n      - description: object type\n        enum:\n        - question\n        - answer\n        - tag\n        in: query\n        name: object_type\n        type: string\n      - description: is show vote\n        in: query\n        name: show_vote\n        type: boolean\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetObjectTimelineResp'\n              type: object\n      summary: get object timeline\n      tags:\n      - Comment\n  /answer/api/v1/activity/timeline/detail:\n    get:\n      description: get object timeline detail\n      parameters:\n      - description: revision id\n        in: query\n        name: revision_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetObjectTimelineResp'\n              type: object\n      summary: get object timeline detail\n      tags:\n      - Comment\n  /answer/api/v1/ai/conversation:\n    get:\n      consumes:\n      - application/json\n      description: get conversation detail\n      parameters:\n      - description: conversation id\n        in: query\n        name: conversation_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.AIConversationDetailResp'\n              type: object\n      summary: get conversation detail\n      tags:\n      - ai-conversation\n  /answer/api/v1/ai/conversation/page:\n    get:\n      consumes:\n      - application/json\n      description: get conversation list\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.AIConversationListItem'\n                        type: array\n                    type: object\n              type: object\n      summary: get conversation list\n      tags:\n      - ai-conversation\n  /answer/api/v1/ai/conversation/vote:\n    post:\n      consumes:\n      - application/json\n      description: vote record\n      parameters:\n      - description: vote request\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AIConversationVoteReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: vote record\n      tags:\n      - ai-conversation\n  /answer/api/v1/answer:\n    delete:\n      consumes:\n      - application/json\n      description: delete answer\n      parameters:\n      - description: answer\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RemoveAnswerReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: delete answer\n      tags:\n      - Answer\n    post:\n      consumes:\n      - application/json\n      description: add answer\n      parameters:\n      - description: add answer request\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AnswerAddReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Add Answer\n      tags:\n      - Answer\n    put:\n      consumes:\n      - application/json\n      description: Update Answer\n      parameters:\n      - description: AnswerUpdateReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AnswerUpdateReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Update Answer\n      tags:\n      - Answer\n  /answer/api/v1/answer/acceptance:\n    post:\n      consumes:\n      - application/json\n      description: Accept Answer\n      parameters:\n      - description: AcceptAnswerReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AcceptAnswerReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Accept Answer\n      tags:\n      - Answer\n  /answer/api/v1/answer/info:\n    get:\n      consumes:\n      - application/json\n      description: Get Answer Detail\n      parameters:\n      - description: id\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetAnswerInfoResp'\n              type: object\n      summary: Get Answer Detail\n      tags:\n      - Answer\n  /answer/api/v1/answer/page:\n    get:\n      consumes:\n      - application/json\n      description: AnswerList <br> <b>order</b> (default or updated)\n      parameters:\n      - description: question_id\n        in: query\n        name: question_id\n        required: true\n        type: string\n      - description: order\n        in: query\n        name: order\n        required: true\n        type: string\n      - description: page\n        in: query\n        name: page\n        required: true\n        type: string\n      - description: page_size\n        in: query\n        name: page_size\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: AnswerList\n      tags:\n      - Answer\n  /answer/api/v1/answer/recover:\n    post:\n      consumes:\n      - application/json\n      description: recover the deleted answer\n      parameters:\n      - description: answer\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RecoverAnswerReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: recover answer\n      tags:\n      - Answer\n  /answer/api/v1/badge:\n    get:\n      consumes:\n      - application/json\n      description: get badge info\n      parameters:\n      - default: string\n        description: id\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetBadgeInfoResp'\n              type: object\n      summary: get badge info\n      tags:\n      - api-badge\n  /answer/api/v1/badge/awards/page:\n    get:\n      consumes:\n      - application/json\n      description: get badge award list\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: badge id\n        in: query\n        name: badge_id\n        required: true\n        type: string\n      - description: only list the award by username\n        in: query\n        name: username\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetBadgeInfoResp'\n              type: object\n      summary: get badge award list\n      tags:\n      - api-badge\n  /answer/api/v1/badge/user/awards:\n    get:\n      consumes:\n      - application/json\n      description: get user badge award list\n      parameters:\n      - description: user name\n        in: query\n        name: username\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetUserBadgeAwardListResp'\n                  type: array\n              type: object\n      summary: get user badge award list\n      tags:\n      - api-badge\n  /answer/api/v1/badge/user/awards/recent:\n    get:\n      consumes:\n      - application/json\n      description: get user badge award list\n      parameters:\n      - description: user name\n        in: query\n        name: username\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetUserBadgeAwardListResp'\n                  type: array\n              type: object\n      summary: get user badge award list\n      tags:\n      - api-badge\n  /answer/api/v1/badges:\n    get:\n      consumes:\n      - application/json\n      description: list all badges group by group\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetBadgeListResp'\n                  type: array\n              type: object\n      summary: list all badges group by group\n      tags:\n      - api-badge\n  /answer/api/v1/collection/switch:\n    post:\n      consumes:\n      - application/json\n      description: add collection\n      parameters:\n      - description: collection\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.CollectionSwitchReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.CollectionSwitchResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: add collection\n      tags:\n      - Collection\n  /answer/api/v1/comment:\n    delete:\n      consumes:\n      - application/json\n      description: remove comment\n      parameters:\n      - description: comment\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RemoveCommentReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: remove comment\n      tags:\n      - Comment\n    get:\n      description: get comment by id\n      parameters:\n      - description: id\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetCommentResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get comment by id\n      tags:\n      - Comment\n    post:\n      consumes:\n      - application/json\n      description: add comment\n      parameters:\n      - description: comment\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddCommentReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetCommentResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: add comment\n      tags:\n      - Comment\n    put:\n      consumes:\n      - application/json\n      description: update comment\n      parameters:\n      - description: comment\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateCommentReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update comment\n      tags:\n      - Comment\n  /answer/api/v1/comment/page:\n    get:\n      description: get comment page\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: object id\n        in: query\n        name: object_id\n        required: true\n        type: string\n      - description: query condition\n        enum:\n        - vote\n        in: query\n        name: query_cond\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetCommentResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get comment page\n      tags:\n      - Comment\n  /answer/api/v1/connector/binding/email:\n    post:\n      consumes:\n      - application/json\n      description: external login binding user send email\n      parameters:\n      - description: external login binding user send email\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp'\n              type: object\n      summary: external login binding user send email\n      tags:\n      - PluginConnector\n  /answer/api/v1/connector/info:\n    get:\n      description: get all enabled connectors\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.ConnectorInfoResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get all enabled connectors\n      tags:\n      - PluginConnector\n  /answer/api/v1/connector/user/info:\n    get:\n      description: get all connectors info about user\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.ConnectorUserInfoResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get all connectors info about user\n      tags:\n      - PluginConnector\n  /answer/api/v1/connector/user/unbinding:\n    delete:\n      consumes:\n      - application/json\n      description: unbind external user login\n      parameters:\n      - description: ExternalLoginUnbindingReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.ExternalLoginUnbindingReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: unbind external user login\n      tags:\n      - PluginConnector\n  /answer/api/v1/embed/config:\n    get:\n      consumes:\n      - application/json\n      description: get embed plugin config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/plugin.EmbedConfig'\n                  type: array\n              type: object\n      summary: get embed plugin config\n      tags:\n      - Plugin\n  /answer/api/v1/file:\n    post:\n      consumes:\n      - multipart/form-data\n      description: upload file\n      parameters:\n      - description: identify the source of the file upload\n        enum:\n        - post\n        - post_attachment\n        - avatar\n        - branding\n        in: formData\n        name: source\n        required: true\n        type: string\n      - description: file\n        in: formData\n        name: file\n        required: true\n        type: file\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  type: string\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: upload file\n      tags:\n      - Upload\n  /answer/api/v1/follow:\n    post:\n      consumes:\n      - application/json\n      description: follow object or cancel follow operation\n      parameters:\n      - description: follow\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.FollowReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.FollowResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: follow object or cancel follow operation\n      tags:\n      - Activity\n  /answer/api/v1/follow/tags:\n    put:\n      consumes:\n      - application/json\n      description: update user follow tags\n      parameters:\n      - description: follow\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateFollowTagsReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user follow tags\n      tags:\n      - Activity\n  /answer/api/v1/language/config:\n    get:\n      description: get language config mapping\n      parameters:\n      - description: Accept-Language\n        in: header\n        name: Accept-Language\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: get language config mapping\n      tags:\n      - Lang\n  /answer/api/v1/language/options:\n    get:\n      description: Get language options\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: Get language options\n      tags:\n      - Lang\n  /answer/api/v1/meta/reaction:\n    get:\n      consumes:\n      - application/json\n      description: get reaction for an object\n      parameters:\n      - description: object_id\n        in: query\n        name: object_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.ReactionRespItem'\n              type: object\n      summary: get reaction\n      tags:\n      - Meta\n    put:\n      consumes:\n      - application/json\n      description: update reaction. if not exist, add one\n      parameters:\n      - description: reaction\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateReactionReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add or update reaction\n      tags:\n      - Meta\n  /answer/api/v1/notification/page:\n    get:\n      consumes:\n      - application/json\n      description: get notification list\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: type\n        enum:\n        - inbox\n        - achievement\n        in: query\n        name: type\n        required: true\n        type: string\n      - description: inbox_type\n        enum:\n        - all\n        - posts\n        - invites\n        - votes\n        in: query\n        name: inbox_type\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: get notification list\n      tags:\n      - Notification\n  /answer/api/v1/notification/read/state:\n    put:\n      consumes:\n      - application/json\n      description: ClearUnRead\n      parameters:\n      - description: NotificationClearIDRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.NotificationClearIDRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: ClearUnRead\n      tags:\n      - Notification\n  /answer/api/v1/notification/read/state/all:\n    put:\n      consumes:\n      - application/json\n      description: ClearUnRead\n      parameters:\n      - description: NotificationClearRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.NotificationClearRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: ClearUnRead\n      tags:\n      - Notification\n  /answer/api/v1/notification/status:\n    get:\n      consumes:\n      - application/json\n      description: GetRedDot\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: GetRedDot\n      tags:\n      - Notification\n    put:\n      consumes:\n      - application/json\n      description: DelRedDot\n      parameters:\n      - description: NotificationClearRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.NotificationClearRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: DelRedDot\n      tags:\n      - Notification\n  /answer/api/v1/permission:\n    get:\n      description: check user permission\n      parameters:\n      - description: access-token\n        in: header\n        name: Authorization\n        required: true\n        type: string\n      - description: permission key\n        enum:\n        - question.add\n        - question.edit\n        - question.edit_without_review\n        - question.delete\n        - question.close\n        - question.reopen\n        - question.vote_up\n        - question.vote_down\n        - question.pin\n        - question.unpin\n        - question.hide\n        - question.show\n        - answer.add\n        - answer.edit\n        - answer.edit_without_review\n        - answer.delete\n        - answer.accept\n        - answer.vote_up\n        - answer.vote_down\n        - answer.invite_someone_to_answer\n        - comment.add\n        - comment.edit\n        - comment.delete\n        - comment.vote_up\n        - comment.vote_down\n        - report.add\n        - tag.add\n        - tag.edit\n        - tag.edit_slug_name\n        - tag.edit_without_review\n        - tag.delete\n        - tag.synonym\n        - link.url_limit\n        - vote.detail\n        - answer.audit\n        - question.audit\n        - tag.audit\n        - tag.use_reserved_tag\n        in: query\n        name: action\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  additionalProperties:\n                    type: boolean\n                  type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: check user permission\n      tags:\n      - Permission\n  /answer/api/v1/personal/answer/page:\n    get:\n      consumes:\n      - application/json\n      description: list personal answers\n      parameters:\n      - default: string\n        description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      - description: order\n        enum:\n        - newest\n        - score\n        in: query\n        name: order\n        required: true\n        type: string\n      - default: \"0\"\n        description: page\n        in: query\n        name: page\n        required: true\n        type: string\n      - default: \"20\"\n        description: page_size\n        in: query\n        name: page_size\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: list personal answers\n      tags:\n      - Personal\n  /answer/api/v1/personal/collection/page:\n    get:\n      consumes:\n      - application/json\n      description: list personal collections\n      parameters:\n      - default: \"0\"\n        description: page\n        in: query\n        name: page\n        required: true\n        type: string\n      - default: \"20\"\n        description: page_size\n        in: query\n        name: page_size\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: list personal collections\n      tags:\n      - Collection\n  /answer/api/v1/personal/comment/page:\n    get:\n      description: user personal comment list\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: username\n        in: query\n        name: username\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetCommentPersonalWithPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: user personal comment list\n      tags:\n      - Comment\n  /answer/api/v1/personal/qa/top:\n    get:\n      consumes:\n      - application/json\n      description: UserTop\n      parameters:\n      - default: string\n        description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: UserTop\n      tags:\n      - Question\n  /answer/api/v1/personal/rank/page:\n    get:\n      description: user personal rank list\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: username\n        in: query\n        name: username\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetRankPersonalPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: user personal rank list\n      tags:\n      - Rank\n  /answer/api/v1/personal/user/info:\n    get:\n      consumes:\n      - application/json\n      description: GetOtherUserInfoByUsername\n      parameters:\n      - description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetOtherUserInfoResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: GetOtherUserInfoByUsername\n      tags:\n      - User\n  /answer/api/v1/personal/vote/page:\n    get:\n      consumes:\n      - application/json\n      description: get user personal votes\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetVoteWithPageResp'\n                        type: array\n                    type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get user personal votes\n      tags:\n      - Activity\n  /answer/api/v1/plugin/status:\n    get:\n      consumes:\n      - application/json\n      description: get all plugins status\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetPluginListResp'\n                  type: array\n              type: object\n      summary: get all plugins status\n      tags:\n      - Plugin\n  /answer/api/v1/post/render:\n    post:\n      consumes:\n      - application/json\n      description: render post content\n      parameters:\n      - description: PostRenderReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.PostRenderReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: render post content\n      tags:\n      - Upload\n  /answer/api/v1/question:\n    delete:\n      consumes:\n      - application/json\n      description: delete question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RemoveQuestionReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: delete question\n      tags:\n      - Question\n    post:\n      consumes:\n      - application/json\n      description: add question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionAdd'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add question\n      tags:\n      - Question\n    put:\n      consumes:\n      - application/json\n      description: update question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionUpdate'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update question\n      tags:\n      - Question\n  /answer/api/v1/question/answer:\n    post:\n      consumes:\n      - application/json\n      description: add question and answer\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionAddByAnswer'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add question and answer\n      tags:\n      - Question\n  /answer/api/v1/question/info:\n    get:\n      consumes:\n      - application/json\n      description: get question details\n      parameters:\n      - default: \"1\"\n        description: Question TagID\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: get question details\n      tags:\n      - Question\n  /answer/api/v1/question/invite:\n    get:\n      consumes:\n      - application/json\n      description: get question invite user info\n      parameters:\n      - default: \"1\"\n        description: Question ID\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: get question invite user info\n      tags:\n      - Question\n    put:\n      consumes:\n      - application/json\n      description: update question invite user\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionUpdateInviteUser'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update question invite user\n      tags:\n      - Question\n  /answer/api/v1/question/link:\n    get:\n      description: get question link\n      parameters:\n      - in: query\n        minimum: 1\n        name: in_days\n        type: integer\n      - enum:\n        - newest\n        - active\n        - hot\n        - score\n        - unanswered\n        - recommend\n        - frequent\n        in: query\n        name: order\n        type: string\n      - in: query\n        minimum: 1\n        name: page\n        type: integer\n      - in: query\n        maximum: 100\n        minimum: 1\n        name: page_size\n        type: integer\n      - in: query\n        name: question_id\n        required: true\n        type: string\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.QuestionPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get question link\n      tags:\n      - Question\n  /answer/api/v1/question/operation:\n    put:\n      consumes:\n      - application/json\n      description: Operation question \\n operation [pin unpin hide show]\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.OperationQuestionReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Operation question\n      tags:\n      - Question\n  /answer/api/v1/question/page:\n    get:\n      consumes:\n      - application/json\n      description: get questions by page\n      parameters:\n      - description: QuestionPageReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionPageReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.QuestionPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get questions by page\n      tags:\n      - Question\n  /answer/api/v1/question/recommend/page:\n    get:\n      consumes:\n      - application/json\n      description: get recommend questions by page\n      parameters:\n      - description: QuestionPageReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionPageReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.QuestionPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get recommend questions by page\n      tags:\n      - Question\n  /answer/api/v1/question/recover:\n    post:\n      consumes:\n      - application/json\n      description: recover deleted question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.QuestionRecoverReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: recover deleted question\n      tags:\n      - Question\n  /answer/api/v1/question/reopen:\n    put:\n      consumes:\n      - application/json\n      description: reopen question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.ReopenQuestionReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: reopen question\n      tags:\n      - Question\n  /answer/api/v1/question/similar:\n    get:\n      consumes:\n      - application/json\n      description: fuzzy query similar questions based on title\n      parameters:\n      - default: string\n        description: title\n        in: query\n        name: title\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: fuzzy query similar questions based on title\n      tags:\n      - Question\n  /answer/api/v1/question/similar/tag:\n    get:\n      consumes:\n      - application/json\n      description: Search Similar Question\n      parameters:\n      - default: \"\"\n        description: question_id\n        in: query\n        name: question_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: Search Similar Question\n      tags:\n      - Question\n  /answer/api/v1/question/status:\n    put:\n      consumes:\n      - application/json\n      description: Close question\n      parameters:\n      - description: question\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.CloseQuestionReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: Close question\n      tags:\n      - Question\n  /answer/api/v1/question/tags:\n    get:\n      description: get tag list\n      parameters:\n      - description: tag\n        in: query\n        name: tag\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetTagBasicResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get tag list\n      tags:\n      - Tag\n  /answer/api/v1/reasons:\n    get:\n      consumes:\n      - application/json\n      description: get reasons by object type and action\n      parameters:\n      - description: object_type\n        enum:\n        - question\n        - answer\n        - comment\n        - user\n        in: query\n        name: object_type\n        required: true\n        type: string\n      - description: action\n        enum:\n        - status\n        - close\n        - flag\n        - review\n        in: query\n        name: action\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: get reasons by object type and action\n      tags:\n      - reason\n  /answer/api/v1/render/config:\n    get:\n      consumes:\n      - application/json\n      description: GetRenderConfig\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/plugin.RenderConfig'\n              type: object\n      summary: GetRenderConfig\n      tags:\n      - PluginRender\n  /answer/api/v1/report:\n    post:\n      consumes:\n      - application/json\n      description: add report <br> source (question, answer, comment, user)\n      parameters:\n      - description: report\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddReportReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add report\n      tags:\n      - Report\n  /answer/api/v1/report/review:\n    put:\n      consumes:\n      - application/json\n      description: review report\n      parameters:\n      - description: flag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.ReviewReportReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: review report\n      tags:\n      - Report\n  /answer/api/v1/report/unreviewed/post:\n    get:\n      consumes:\n      - application/json\n      description: get unreviewed report post page\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetReportListPageResp'\n                        type: array\n                    type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get unreviewed report post page\n      tags:\n      - Report\n  /answer/api/v1/review/pending/post:\n    put:\n      consumes:\n      - application/json\n      description: update review\n      parameters:\n      - description: review\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateReviewReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update review\n      tags:\n      - Review\n  /answer/api/v1/review/pending/post/page:\n    get:\n      consumes:\n      - application/json\n      description: get unreviewed post page\n      parameters:\n      - description: page\n        in: query\n        name: page\n        type: integer\n      - description: object_id\n        in: query\n        name: object_id\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetUnreviewedPostPageResp'\n                        type: array\n                    type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get unreviewed post page\n      tags:\n      - Review\n  /answer/api/v1/reviewing/type:\n    get:\n      description: get reviewing type\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetReviewingTypeResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get reviewing type\n      tags:\n      - Revision\n  /answer/api/v1/revisions:\n    get:\n      description: get revision list\n      parameters:\n      - description: object id\n        in: query\n        name: object_id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetRevisionResp'\n                  type: array\n              type: object\n      summary: get revision list\n      tags:\n      - Revision\n  /answer/api/v1/revisions/audit:\n    put:\n      description: revision audit operation:approve or reject\n      parameters:\n      - description: audit\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RevisionAuditReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: revision audit\n      tags:\n      - Revision\n  /answer/api/v1/revisions/edit/check:\n    get:\n      consumes:\n      - application/json\n      description: check can update revision\n      parameters:\n      - default: string\n        description: id\n        in: query\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: check can update revision\n      tags:\n      - Revision\n  /answer/api/v1/revisions/unreviewed:\n    get:\n      description: get unreviewed revision list\n      parameters:\n      - description: page id\n        in: query\n        name: page\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetUnreviewedRevisionResp'\n                        type: array\n                    type: object\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get unreviewed revision list\n      tags:\n      - Revision\n  /answer/api/v1/search:\n    get:\n      description: search object\n      parameters:\n      - description: query string\n        in: query\n        name: q\n        required: true\n        type: string\n      - description: order\n        enum:\n        - newest\n        - active\n        - score\n        - relevance\n        in: query\n        name: order\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SearchResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: search object\n      tags:\n      - Search\n  /answer/api/v1/search/desc:\n    get:\n      description: get search description\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SearchResp'\n              type: object\n      summary: get search description\n      tags:\n      - Search\n  /answer/api/v1/siteinfo:\n    get:\n      description: get site info\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.SiteInfoResp'\n              type: object\n      summary: get site info\n      tags:\n      - site\n  /answer/api/v1/siteinfo/legal:\n    get:\n      description: get site legal info\n      parameters:\n      - description: legal information type\n        enum:\n        - tos\n        - privacy\n        in: query\n        name: info_type\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetSiteLegalInfoResp'\n              type: object\n      summary: get site legal info\n      tags:\n      - site\n  /answer/api/v1/tag:\n    delete:\n      consumes:\n      - application/json\n      description: delete tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RemoveTagReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: delete tag\n      tags:\n      - Tag\n    get:\n      consumes:\n      - application/json\n      description: get tag one\n      parameters:\n      - description: tag id\n        in: query\n        name: tag_id\n        required: true\n        type: string\n      - description: tag name\n        in: query\n        name: tag_name\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetTagResp'\n              type: object\n      summary: get tag one\n      tags:\n      - Tag\n    post:\n      consumes:\n      - application/json\n      description: add tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddTagReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: add tag\n      tags:\n      - Tag\n    put:\n      consumes:\n      - application/json\n      description: update tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateTagReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update tag\n      tags:\n      - Tag\n  /answer/api/v1/tag/merge:\n    post:\n      consumes:\n      - application/json\n      description: merge tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.AddTagReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: merge tag\n      tags:\n      - Tag\n  /answer/api/v1/tag/recover:\n    post:\n      consumes:\n      - application/json\n      description: recover delete tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.RecoverTagReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: recover delete tag\n      tags:\n      - Tag\n  /answer/api/v1/tag/synonym:\n    put:\n      consumes:\n      - application/json\n      description: update tag\n      parameters:\n      - description: tag\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateTagSynonymReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update tag\n      tags:\n      - Tag\n  /answer/api/v1/tag/synonyms:\n    get:\n      description: get tag synonyms\n      parameters:\n      - description: tag id\n        in: query\n        name: tag_id\n        required: true\n        type: integer\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetTagSynonymsResp'\n              type: object\n      summary: get tag synonyms\n      tags:\n      - Tag\n  /answer/api/v1/tags:\n    get:\n      description: get tags list by slug name\n      parameters:\n      - collectionFormat: csv\n        description: string collection\n        in: query\n        items:\n          type: string\n        name: tags\n        type: array\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetTagBasicResp'\n                  type: array\n              type: object\n      summary: get tags list\n      tags:\n      - Tag\n  /answer/api/v1/tags/following:\n    get:\n      description: get following tag list\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetFollowingTagsResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get following tag list\n      tags:\n      - Tag\n  /answer/api/v1/tags/page:\n    get:\n      description: get tag page\n      parameters:\n      - description: page size\n        in: query\n        name: page\n        type: integer\n      - description: page size\n        in: query\n        name: page_size\n        type: integer\n      - description: slug_name\n        in: query\n        name: slug_name\n        type: string\n      - description: query condition\n        enum:\n        - popular\n        - name\n        - newest\n        in: query\n        name: query_cond\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  allOf:\n                  - $ref: '#/definitions/pager.PageModel'\n                  - properties:\n                      list:\n                        items:\n                          $ref: '#/definitions/schema.GetTagPageResp'\n                        type: array\n                    type: object\n              type: object\n      summary: get tag page\n      tags:\n      - Tag\n  /answer/api/v1/user/action/record:\n    get:\n      description: ActionRecord\n      parameters:\n      - description: action\n        enum:\n        - login\n        - e_mail\n        - find_pass\n        in: query\n        name: action\n        required: true\n        type: string\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.ActionRecordResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: ActionRecord\n      tags:\n      - User\n  /answer/api/v1/user/email:\n    put:\n      consumes:\n      - application/json\n      description: user change email verification\n      parameters:\n      - description: UserChangeEmailVerifyReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserChangeEmailVerifyReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: user change email verification\n      tags:\n      - User\n  /answer/api/v1/user/email/change/code:\n    post:\n      consumes:\n      - application/json\n      description: send email to the user email then change their email\n      parameters:\n      - description: UserChangeEmailSendCodeReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserChangeEmailSendCodeReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: send email to the user email then change their email\n      tags:\n      - User\n  /answer/api/v1/user/email/verification:\n    post:\n      consumes:\n      - application/json\n      description: UserVerifyEmail\n      parameters:\n      - default: \"\"\n        description: code\n        in: query\n        name: code\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.UserLoginResp'\n              type: object\n      summary: UserVerifyEmail\n      tags:\n      - User\n  /answer/api/v1/user/email/verification/send:\n    post:\n      consumes:\n      - application/json\n      description: UserVerifyEmailSend\n      parameters:\n      - default: \"\"\n        description: captcha_id\n        in: query\n        name: captcha_id\n        type: string\n      - default: \"\"\n        description: captcha_code\n        in: query\n        name: captcha_code\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      security:\n      - ApiKeyAuth: []\n      summary: UserVerifyEmailSend\n      tags:\n      - User\n  /answer/api/v1/user/info:\n    get:\n      consumes:\n      - application/json\n      description: get user info, if user no login response http code is 200, but\n        user info is null\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetCurrentLoginUserInfoResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: GetUserInfoByUserID\n      tags:\n      - User\n    put:\n      consumes:\n      - application/json\n      description: UserUpdateInfo update user info\n      parameters:\n      - description: access-token\n        in: header\n        name: Authorization\n        required: true\n        type: string\n      - description: UpdateInfoRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateInfoRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: UserUpdateInfo update user info\n      tags:\n      - User\n  /answer/api/v1/user/info/search:\n    get:\n      consumes:\n      - application/json\n      description: SearchUserListByName\n      parameters:\n      - description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetOtherUserInfoResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: SearchUserListByName\n      tags:\n      - User\n  /answer/api/v1/user/interface:\n    put:\n      consumes:\n      - application/json\n      description: UserUpdateInterface update user interface config\n      parameters:\n      - description: access-token\n        in: header\n        name: Authorization\n        required: true\n        type: string\n      - description: UpdateInfoRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserInterfaceRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: UserUpdateInterface update user interface config\n      tags:\n      - User\n  /answer/api/v1/user/login/email:\n    post:\n      consumes:\n      - application/json\n      description: UserEmailLogin\n      parameters:\n      - description: UserEmailLogin\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserEmailLoginReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.UserLoginResp'\n              type: object\n      summary: UserEmailLogin\n      tags:\n      - User\n  /answer/api/v1/user/logout:\n    get:\n      consumes:\n      - application/json\n      description: user logout\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: user logout\n      tags:\n      - User\n  /answer/api/v1/user/notification/config:\n    post:\n      consumes:\n      - application/json\n      description: get user's notification config\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetUserNotificationConfigResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get user's notification config\n      tags:\n      - User\n    put:\n      consumes:\n      - application/json\n      description: update user's notification config\n      parameters:\n      - description: UpdateUserNotificationConfigReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserNotificationConfigReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user's notification config\n      tags:\n      - User\n  /answer/api/v1/user/notification/unsubscribe:\n    put:\n      consumes:\n      - application/json\n      description: unsubscribe notification\n      parameters:\n      - description: UserUnsubscribeNotificationReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserUnsubscribeNotificationReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: unsubscribe notification\n      tags:\n      - User\n  /answer/api/v1/user/password:\n    put:\n      consumes:\n      - application/json\n      description: UserModifyPassWord\n      parameters:\n      - description: UserModifyPasswordReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserModifyPasswordReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: UserModifyPassWord\n      tags:\n      - User\n  /answer/api/v1/user/password/replacement:\n    post:\n      consumes:\n      - application/json\n      description: UseRePassWord\n      parameters:\n      - description: UserRePassWordRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserRePassWordRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: UseRePassWord\n      tags:\n      - User\n  /answer/api/v1/user/password/reset:\n    post:\n      consumes:\n      - application/json\n      description: RetrievePassWord\n      parameters:\n      - description: UserRetrievePassWordRequest\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserRetrievePassWordRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: RetrievePassWord\n      tags:\n      - User\n  /answer/api/v1/user/plugin/config:\n    get:\n      description: get user plugin config\n      parameters:\n      - description: plugin_slug_name\n        in: query\n        name: plugin_slug_name\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetPluginConfigResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get user plugin config\n      tags:\n      - UserPlugin\n    put:\n      consumes:\n      - application/json\n      description: update user plugin config\n      parameters:\n      - description: UpdatePluginConfigReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UpdateUserPluginConfigReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: update user plugin config\n      tags:\n      - UserPlugin\n  /answer/api/v1/user/plugin/configs:\n    get:\n      consumes:\n      - application/json\n      description: get plugin list that used for user.\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/schema.GetUserPluginListResp'\n                  type: array\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: get plugin list that used for user.\n      tags:\n      - UserPlugin\n  /answer/api/v1/user/ranking:\n    get:\n      consumes:\n      - application/json\n      description: get user ranking\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.UserRankingResp'\n              type: object\n      summary: get user ranking\n      tags:\n      - User\n  /answer/api/v1/user/register/email:\n    post:\n      consumes:\n      - application/json\n      description: UserRegisterByEmail\n      parameters:\n      - description: UserRegisterReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.UserRegisterReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.UserLoginResp'\n              type: object\n      summary: UserRegisterByEmail\n      tags:\n      - User\n  /answer/api/v1/user/staff:\n    get:\n      consumes:\n      - application/json\n      description: get user staff\n      parameters:\n      - description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      - description: page_size\n        in: query\n        name: page_size\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.GetUserStaffResp'\n              type: object\n      summary: get user staff\n      tags:\n      - User\n  /answer/api/v1/vote/down:\n    post:\n      consumes:\n      - application/json\n      description: add vote\n      parameters:\n      - description: vote\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.VoteReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.VoteResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: vote down\n      tags:\n      - Activity\n  /answer/api/v1/vote/up:\n    post:\n      consumes:\n      - application/json\n      description: add vote\n      parameters:\n      - description: vote\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/schema.VoteReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/schema.VoteResp'\n              type: object\n      security:\n      - ApiKeyAuth: []\n      summary: vote up\n      tags:\n      - Activity\n  /custom.css:\n    get:\n      description: get site custom CSS\n      produces:\n      - text/css\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: get site custom CSS\n      tags:\n      - site\n  /installation/base-info:\n    post:\n      consumes:\n      - application/json\n      description: init base info\n      parameters:\n      - description: InitBaseInfoReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/install.InitBaseInfoReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: init base info\n      tags:\n      - installation\n  /installation/config-file/check:\n    post:\n      consumes:\n      - application/json\n      description: check config file if exist when installation\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/install.CheckConfigFileResp'\n              type: object\n      summary: check config file if exist when installation\n      tags:\n      - installation\n  /installation/db/check:\n    post:\n      consumes:\n      - application/json\n      description: check database if exist when installation\n      parameters:\n      - description: CheckDatabaseReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/install.CheckDatabaseReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  $ref: '#/definitions/install.CheckConfigFileResp'\n              type: object\n      summary: check database if exist when installation\n      tags:\n      - installation\n  /installation/init:\n    post:\n      consumes:\n      - application/json\n      description: init environment\n      parameters:\n      - description: CheckDatabaseReq\n        in: body\n        name: data\n        required: true\n        schema:\n          $ref: '#/definitions/install.CheckDatabaseReq'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: init environment\n      tags:\n      - installation\n  /installation/language/config:\n    get:\n      description: get installation language config mapping\n      parameters:\n      - description: installation language\n        in: query\n        name: lang\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      summary: get installation language config mapping\n      tags:\n      - Lang\n  /installation/language/options:\n    get:\n      description: get installation language options\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            allOf:\n            - $ref: '#/definitions/handler.RespBody'\n            - properties:\n                data:\n                  items:\n                    $ref: '#/definitions/translator.LangOption'\n                  type: array\n              type: object\n      summary: get installation language options\n      tags:\n      - Lang\n  /personal/question/page:\n    get:\n      consumes:\n      - application/json\n      description: list personal questions\n      parameters:\n      - default: string\n        description: username\n        in: query\n        name: username\n        required: true\n        type: string\n      - description: order\n        enum:\n        - newest\n        - score\n        in: query\n        name: order\n        required: true\n        type: string\n      - default: \"0\"\n        description: page\n        in: query\n        name: page\n        required: true\n        type: string\n      - default: \"20\"\n        description: page_size\n        in: query\n        name: page_size\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/handler.RespBody'\n      security:\n      - ApiKeyAuth: []\n      summary: list personal questions\n      tags:\n      - Personal\n  /robots.txt:\n    get:\n      description: get site robots information\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n      summary: get site robots information\n      tags:\n      - site\nsecurityDefinitions:\n  ApiKeyAuth:\n    in: header\n    name: Authorization\n    type: apiKey\nswagger: \"2.0\"\n"
  },
  {
    "path": "go.mod",
    "content": "// Licensed to the Apache Software Foundation (ASF) under one\n// or more contributor license agreements.  See the NOTICE file\n// distributed with this work for additional information\n// regarding copyright ownership.  The ASF licenses this file\n// to you under the Apache License, Version 2.0 (the\n// \"License\"); you may not use this file except in compliance\n// with the License.  You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing,\n// software distributed under the License is distributed on an\n// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n// KIND, either express or implied.  See the License for the\n// specific language governing permissions and limitations\n// under the License.\n\nmodule github.com/apache/answer\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/Machiel/slugify v1.0.1\n\tgithub.com/Masterminds/semver/v3 v3.3.0\n\tgithub.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2\n\tgithub.com/bwmarrin/snowflake v0.3.0\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/go-playground/locales v0.14.1\n\tgithub.com/go-playground/universal-translator v0.18.1\n\tgithub.com/go-playground/validator/v10 v10.22.1\n\tgithub.com/go-resty/resty/v2 v2.17.1\n\tgithub.com/go-sql-driver/mysql v1.8.1\n\tgithub.com/goccy/go-json v0.10.3\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/google/wire v0.5.0\n\tgithub.com/grokify/html-strip-tags-go v0.1.0\n\tgithub.com/jinzhu/copier v0.4.0\n\tgithub.com/jinzhu/now v1.1.5\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/mark3labs/mcp-go v0.43.2\n\tgithub.com/microcosm-cc/bluemonday v1.0.27\n\tgithub.com/mozillazg/go-pinyin v0.20.0\n\tgithub.com/ory/dockertest/v3 v3.11.0\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/sashabaranov/go-openai v1.41.2\n\tgithub.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405\n\tgithub.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f\n\tgithub.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f\n\tgithub.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f\n\tgithub.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f\n\tgithub.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f\n\tgithub.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f\n\tgithub.com/spf13/cobra v1.8.1\n\tgithub.com/stretchr/testify v1.9.0\n\tgithub.com/swaggo/files v1.0.1\n\tgithub.com/swaggo/gin-swagger v1.6.0\n\tgithub.com/swaggo/swag v1.16.3\n\tgithub.com/tidwall/gjson v1.17.3\n\tgithub.com/yuin/goldmark v1.7.4\n\tgo.uber.org/mock v0.6.0\n\tgolang.org/x/crypto v0.41.0\n\tgolang.org/x/image v0.20.0\n\tgolang.org/x/net v0.43.0\n\tgolang.org/x/term v0.34.0\n\tgolang.org/x/text v0.28.0\n\tgopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df\n\tgopkg.in/yaml.v3 v3.0.1\n\tmodernc.org/sqlite v1.33.0\n\txorm.io/builder v0.3.13\n\txorm.io/xorm v1.3.2\n)\n\nrequire (\n\tdario.cat/mergo v1.0.1 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect\n\tgithub.com/KyleBanks/depth v1.2.1 // indirect\n\tgithub.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect\n\tgithub.com/andybalholm/brotli v1.1.0 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/bytedance/sonic v1.12.2 // indirect\n\tgithub.com/bytedance/sonic/loader v0.2.0 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // indirect\n\tgithub.com/containerd/continuity v0.4.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/docker/cli v27.2.1+incompatible // indirect\n\tgithub.com/docker/docker v27.2.1+incompatible // indirect\n\tgithub.com/docker/go-connections v0.5.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect\n\tgithub.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect\n\tgithub.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect\n\tgithub.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect\n\tgithub.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect\n\tgithub.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect\n\tgithub.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect\n\tgithub.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.5 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-errors/errors v1.5.1 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.0 // indirect\n\tgithub.com/go-openapi/spec v0.21.0 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.1.0 // indirect\n\tgithub.com/go-xmlfmt/xmlfmt v1.1.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/invopop/jsonschema v0.13.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.8 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect\n\tgithub.com/lestrrat-go/strftime v1.1.0 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/term v0.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.0 // indirect\n\tgithub.com/opencontainers/runc v1.1.14 // indirect\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.3 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/sagikazarmark/locafero v0.6.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.7.1 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/spf13/viper v1.19.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/syndtr/goleveldb v1.0.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgolang.org/x/arch v0.10.0 // indirect\n\tgolang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/tools v0.36.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n\tgopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tmodernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect\n\tmodernc.org/mathutil v1.6.0 // indirect\n\tmodernc.org/memory v1.8.0 // indirect\n\tmodernc.org/strutil v1.2.0 // indirect\n\tmodernc.org/token v1.1.0 // indirect\n\tsigs.k8s.io/yaml v1.4.0 // indirect\n)\n\nreplace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1\n\nreplace modernc.org/cc/v3 v3.40.0 => gitlab.com/cznic/cc/v3 v3.40.0\n\nreplace github.com/lyft/protoc-gen-validate v0.0.13 => github.com/LinkinStars/protoc-gen-validate v0.0.0-20251030022322-3fddbbe5a0e6\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ndario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=\ngitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=\ngitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=\ngithub.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=\ngithub.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=\ngithub.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU=\ngithub.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM=\ngithub.com/LinkinStars/protoc-gen-validate v0.0.0-20251030022322-3fddbbe5a0e6/go.mod h1:Lu7LbM9PBAPmasRqVew2kylj56Z1vH/UUM2REVkLh7k=\ngithub.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=\ngithub.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=\ngithub.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=\ngithub.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=\ngithub.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 h1:vDHsaEcs/Q0dwetADENtwus6W1ccaZ9h3KBTm0d2X0g=\ngithub.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o=\ngithub.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=\ngithub.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=\ngithub.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=\ngithub.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=\ngithub.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=\ngithub.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=\ngithub.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=\ngithub.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=\ngithub.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=\ngithub.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70=\ngithub.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI=\ngithub.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=\ngithub.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk=\ngithub.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs=\ngithub.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=\ngithub.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=\ngithub.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=\ngithub.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=\ngithub.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=\ngithub.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=\ngithub.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=\ngithub.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8=\ngithub.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=\ngithub.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48/go.mod h1:H1hAaFyv9cRV1ywoHvaqVoNSThBvWZ0JarRBcV+FSnE=\ngithub.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo=\ngithub.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0=\ngithub.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=\ngithub.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=\ngithub.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=\ngithub.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=\ngithub.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=\ngithub.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A=\ngithub.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=\ngithub.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8/go.mod h1:Bf0nmcDFFRQBjZwr9qY6c0zTxKQa+Q8YWZmlYxXGxY0=\ngithub.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=\ngithub.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=\ngithub.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=\ngithub.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4=\ngithub.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo=\ngithub.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=\ngithub.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=\ngithub.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=\ngithub.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=\ngithub.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=\ngithub.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=\ngithub.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=\ngithub.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=\ngithub.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=\ngithub.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=\ngithub.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=\ngithub.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=\ngithub.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=\ngithub.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=\ngithub.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=\ngithub.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=\ngithub.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/geo v0.0.0-20190812012225-f41920e961ce/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=\ngithub.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=\ngithub.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=\ngithub.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=\ngithub.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=\ngithub.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=\ngithub.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=\ngithub.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=\ngithub.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=\ngithub.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=\ngithub.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=\ngithub.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=\ngithub.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=\ngithub.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=\ngithub.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=\ngithub.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=\ngithub.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=\ngithub.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=\ngithub.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=\ngithub.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=\ngithub.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=\ngithub.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=\ngithub.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg=\ngithub.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=\ngithub.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=\ngithub.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=\ngithub.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=\ngithub.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=\ngithub.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=\ngithub.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=\ngithub.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=\ngithub.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=\ngithub.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=\ngithub.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=\ngithub.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=\ngithub.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=\ngithub.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=\ngithub.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=\ngithub.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=\ngithub.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=\ngithub.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=\ngithub.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=\ngithub.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=\ngithub.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=\ngithub.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=\ngithub.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=\ngithub.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=\ngithub.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=\ngithub.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=\ngithub.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 h1:2ieGkj4z/YPXVyQ2ayZUg3GwE1pYWd5f1RB6DzAOXKM=\ngithub.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0=\ngithub.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=\ngithub.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f h1:1KHe0uN6p798E7XJZPhZkgm/hXk5CTjisCvFMqaZSKI=\ngithub.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=\ngithub.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f h1:/nA4C3UfWw+3XYVBkgVMY1p3nX3uhl22hL2LW3FNcVs=\ngithub.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=\ngithub.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA=\ngithub.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=\ngithub.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f h1:0mrzVRrQ+mz5MWQSdC1y6dwKWiewYKkpRDqNf3nOhmk=\ngithub.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=\ngithub.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f h1:2gjiRmSj3J/F3A1A22UU1BzO4gQypEZx/4D7c7Ue4Ag=\ngithub.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=\ngithub.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=\ngithub.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=\ngithub.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=\ngithub.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=\ngithub.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=\ngithub.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=\ngithub.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=\ngithub.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=\ngithub.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=\ngithub.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=\ngithub.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=\ngithub.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=\ngithub.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngithub.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=\ngo.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngolang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=\ngolang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=\ngolang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=\ngolang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=\ngolang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=\ngolang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=\ngolang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=\ngolang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=\ngotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nmodernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=\nmodernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=\nmodernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=\nmodernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=\nmodernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=\nmodernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=\nmodernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=\nmodernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=\nmodernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=\nmodernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=\nmodernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=\nmodernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=\nmodernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=\nmodernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=\nmodernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=\nmodernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=\nmodernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=\nmodernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=\nmodernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=\nmodernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=\nmodernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=\nmodernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=\nmodernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=\nmodernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=\nmodernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=\nmodernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=\nmodernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=\nmodernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=\nmodernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=\nmodernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=\nmodernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=\nmodernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=\nmodernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=\nmodernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=\nmodernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=\nmodernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=\nmodernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=\nmodernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=\nmodernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=\nmodernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=\nmodernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=\nmodernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=\nmodernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=\nmodernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=\nmodernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=\nmodernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=\nmodernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=\nmodernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=\nmodernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=\nmodernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=\nmodernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=\nmodernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=\nmodernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=\nmodernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=\nmodernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=\nmodernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=\nmodernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=\nmodernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=\nmodernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=\nmodernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=\nmodernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=\nmodernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=\nmodernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=\nmodernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=\nmodernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=\nmodernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=\nmodernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=\nmodernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=\nmodernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=\nmodernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=\nmodernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=\nmodernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=\nmodernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=\nmodernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=\nmodernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=\nmodernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=\nmodernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=\nmodernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=\nmodernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=\nmodernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=\nmodernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=\nmodernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=\nmodernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=\nmodernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk=\nmodernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8=\nmodernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=\nmodernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=\nmodernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=\nmodernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=\nmodernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\nsourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=\nxorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=\nxorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=\nxorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=\nxorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=\nxorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=\n"
  },
  {
    "path": "i18n/af_ZA.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as &lt;link>\n      head:\n        label: Head\n        text: This will insert before &lt;/head>\n      header:\n        label: Header\n        text: This will insert after &lt;body>\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/ar_SA.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/az_AZ.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/bal_BA.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/ban_ID.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/bn_BD.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/bs_BA.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/ca_ES.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/cs_CZ.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Úspěch.\n    unknown:\n      other: Neznámá chyba.\n    request_format_error:\n      other: Formát požadavku není platný.\n    unauthorized_error:\n      other: Neautorizováno.\n    database_error:\n      other: Chyba datového serveru.\n    forbidden_error:\n      other: Zakázáno.\n    duplicate_request_error:\n      other: Duplicitní odeslání.\n  action:\n    report:\n      other: Nahlásit\n    edit:\n      other: Upravit\n    delete:\n      other: Smazat\n    close:\n      other: Zavřít\n    reopen:\n      other: Znovu otevřít\n    forbidden_error:\n      other: Zakázáno.\n    pin:\n      other: Připnout\n    hide:\n      other: Skrýt\n    unpin:\n      other: Odepnout\n    show:\n      other: Zobrazit\n    invite_someone_to_answer:\n      other: Upravit\n    undelete:\n      other: Obnovit\n    merge:\n      other: Sloučit\n  role:\n    name:\n      user:\n        other: Uživatel\n      admin:\n        other: Administrátor\n      moderator:\n        other: Moderátor\n    description:\n      user:\n        other: Výchozí bez zvláštního přístupu.\n      admin:\n        other: Má plnou kontrolu nad stránkou.\n      moderator:\n        other: Má přístup ke všem příspěvkům kromě admin nastavení.\n  privilege:\n    level_1:\n      description:\n        other: Úroveň 1 (méně reputace je vyžadováno pro soukromý tým, skupinu)\n    level_2:\n      description:\n        other: Úroveň 2 (nízká reputace je vyžadována pro startovací komunitu)\n    level_3:\n      description:\n        other: Úroveň 3 (vysoká reputace je vyžadována pro vyspělou komunitu)\n    level_custom:\n      description:\n        other: Vlastní úroveň\n    rank_question_add_label:\n      other: Položit dotaz\n    rank_answer_add_label:\n      other: Napsat odpověď\n    rank_comment_add_label:\n      other: Napsat komentář\n    rank_report_add_label:\n      other: Nahlásit\n    rank_comment_vote_up_label:\n      other: Hlasovat pro komentář\n    rank_link_url_limit_label:\n      other: Zveřejnit více než 2 odkazy najednou\n    rank_question_vote_up_label:\n      other: Hlasovat pro dotaz\n    rank_answer_vote_up_label:\n      other: Hlasovat pro odpověď\n    rank_question_vote_down_label:\n      other: Hlasovat proti otázce\n    rank_answer_vote_down_label:\n      other: Hlasovat proti odpovědi\n    rank_invite_someone_to_answer_label:\n      other: Pozvěte někoho, aby odpověděl\n    rank_tag_add_label:\n      other: Vytvořit nový štítek\n    rank_tag_edit_label:\n      other: Upravit popis štítku (vyžaduje kontrolu)\n    rank_question_edit_label:\n      other: Upravit dotaz někoho jiného (vyžaduje kontrolu)\n    rank_answer_edit_label:\n      other: Upravit odpověď někoho jiného (vyžaduje kontrolu)\n    rank_question_edit_without_review_label:\n      other: Upravit dotaz někoho jiného (bez kontroly)\n    rank_answer_edit_without_review_label:\n      other: Upravit odpověď někoho jiného (bez kontroly)\n    rank_question_audit_label:\n      other: Zkontrolovat úpravy dotazu\n    rank_answer_audit_label:\n      other: Zkontrolovat úpravy odpovědí\n    rank_tag_audit_label:\n      other: Zkontrolovat úpravy štítků\n    rank_tag_edit_without_review_label:\n      other: Upravit popis štítku (bez kontroly)\n    rank_tag_synonym_label:\n      other: Správa synonym štítků\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Heslo\n  pass:\n    other: Heslo\n  old_pass:\n    other: Current password\n  original_text:\n    other: Tento příspěvek\n  email_or_password_wrong_error:\n    other: Email a heslo nesouhlasí.\n  error:\n    common:\n      invalid_url:\n        other: Neplatná URL.\n      status_invalid:\n        other: Neplatný stav.\n    password:\n      space_invalid:\n        other: Heslo nesmí obsahovat mezery.\n    admin:\n      cannot_update_their_password:\n        other: Nemůžete změnit své heslo.\n      cannot_edit_their_profile:\n        other: Nemůžete upravovat svůj profil.\n      cannot_modify_self_status:\n        other: Nemůžete změnit svůj stav.\n      email_or_password_wrong:\n        other: Email a heslo nesouhlasí.\n    answer:\n      not_found:\n        other: Odpověď nebyla nalezena.\n      cannot_deleted:\n        other: Nemáte právo mazat.\n      cannot_update:\n        other: Nemáte právo aktualizovat.\n      question_closed_cannot_add:\n        other: Dotazy jsou uzavřené a není možno je přidávat.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Nejsou povoleny úpravy komentáře.\n      not_found:\n        other: Komentář nebyl nalezen.\n      cannot_edit_after_deadline:\n        other: Tento komentář byl pro úpravy příliš dlouhý.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email už existuje.\n      need_to_be_verified:\n        other: Email musí být ověřen.\n      verify_url_expired:\n        other: Platnost ověřovacího URL vypršela, pošlete si ověřovací email znovu.\n      illegal_email_domain_error:\n        other: Email z této domény není povolen. Použijte jinou doménu.\n    lang:\n      not_found:\n        other: Jazykový soubor nenalezen.\n    object:\n      captcha_verification_failed:\n        other: Nesprávně vyplněná Captcha.\n      disallow_follow:\n        other: Nemáte oprávnění sledovat.\n      disallow_vote:\n        other: Nemáte oprávnění hlasovat.\n      disallow_vote_your_self:\n        other: Nemůžete hlasovat pro svůj vlastní příspěvek.\n      not_found:\n        other: Objekt nenalezen.\n      verification_failed:\n        other: Ověření se nezdařilo.\n      email_or_password_incorrect:\n        other: Email a heslo nesouhlasí.\n      old_password_verification_failed:\n        other: Ověření starého hesla selhalo\n      new_password_same_as_previous_setting:\n        other: Nové heslo je stejné jako předchozí.\n      already_deleted:\n        other: Tento příspěvek byl odstraněn.\n    meta:\n      object_not_found:\n        other: Meta objekt nenalezen\n    question:\n      already_deleted:\n        other: Tento příspěvek byl odstraněn.\n      under_review:\n        other: Váš příspěvek čeká na kontrolu. Bude viditelný po jeho schválení.\n      not_found:\n        other: Dotaz nenalezen.\n      cannot_deleted:\n        other: Nemáte oprávnění k mazání.\n      cannot_close:\n        other: Nemáte oprávnění k uzavření.\n      cannot_update:\n        other: Nemáte oprávnění pro aktualizaci.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Hodnost reputace nesplňuje podmínku.\n      vote_fail_to_meet_the_condition:\n        other: Děkujeme za zpětnou vazbu. Potřebujete alespoň úroveň {{.Rank}}, abyste mohli hlasovat.\n      no_enough_rank_to_operate:\n        other: Potřebujete alespoň úroveň {{.Rank}} k provedení této akce.\n    report:\n      handle_failed:\n        other: Report selhal.\n      not_found:\n        other: Report nebyl nalezen.\n    tag:\n      already_exist:\n        other: Štítek již existuje.\n      not_found:\n        other: Štítek nebyl nalezen.\n      recommend_tag_not_found:\n        other: Doporučený štítek nebyl nalezen.\n      recommend_tag_enter:\n        other: Zadejte prosím alespoň jeden povinný štítek.\n      not_contain_synonym_tags:\n        other: Nemělo by obsahovat synonyma štítků.\n      cannot_update:\n        other: Nemáte oprávnění pro aktualizaci.\n      is_used_cannot_delete:\n        other: Nemůžete odstranit štítek, který se používá.\n      cannot_set_synonym_as_itself:\n        other: Aktuální štítek nelze jako synonymum stejného štítku.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Jméno odesílatele nemůže být emailová adresa.\n    theme:\n      not_found:\n        other: Motiv nebyl nalezen.\n    revision:\n      review_underway:\n        other: V současné době nelze upravit, čeká na kontrolu.\n      no_permission:\n        other: Nemáte oprávnění k revizi.\n    user:\n      external_login_missing_user_id:\n        other: Platforma třetí strany neposkytuje unikátní UserID, takže se nemůžete přihlásit, kontaktujte prosím správce webových stránek.\n      external_login_unbinding_forbidden:\n        other: Před odebráním tohoto typu přihlášení nastavte přihlašovací heslo pro svůj účet.\n      email_or_password_wrong:\n        other:\n          other: Email a heslo nesouhlasí.\n      not_found:\n        other: Uživatel nebyl nalezen.\n      suspended:\n        other: Uživatelský účet byl pozastaven.\n      username_invalid:\n        other: Uživatelské jméno je neplatné.\n      username_duplicate:\n        other: Uživatelské jméno je již použito.\n      set_avatar:\n        other: Nastavení avataru se nezdařilo.\n      cannot_update_your_role:\n        other: Nemůžete upravovat svoji roli.\n      not_allowed_registration:\n        other: Registrace nejsou povolené.\n      not_allowed_login_via_password:\n        other: Přihlášení přes heslo není povolené.\n      access_denied:\n        other: Přístup zamítnut\n      page_access_denied:\n        other: Nemáte přístup k této stránce.\n      add_bulk_users_format_error:\n        other: \"Chyba formátu pole {{.Field}} poblíž '{{.Content}}' na řádku {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Počet uživatelů, které přidáte najednou, by měl být v rozsahu 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Načtení konfigurace selhalo\n    database:\n      connection_failed:\n        other: Spojení s databází selhalo\n      create_table_failed:\n        other: Vytvoření tabulky selhalo\n    install:\n      create_config_failed:\n        other: Soubor config.yaml nelze vytvořit.\n    upload:\n      unsupported_file_format:\n        other: Nepodporovaný formát souboru.\n    site_info:\n      config_not_found:\n        other: Konfigurace webu nebyla nalezena.\n    badge:\n      object_not_found:\n        other: Objekt odznaku nebyl nalezen\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Tento příspěvek je reklama nebo vandalismus. Není užitečný ani relevantní pro aktuální téma.\n    rude_or_abusive:\n      name:\n        other: hrubý nebo zneužívající\n      desc:\n        other: \"Rozumný člověk by tento obsah považoval za nevhodný pro slušnou konverzaci.\"\n    a_duplicate:\n      name:\n        other: duplicita\n      desc:\n        other: Tento dotaz byl položen dříve a již má odpověď.\n      placeholder:\n        other: Zadejte existující odkaz na dotaz\n    not_a_answer:\n      name:\n        other: není odpověď\n      desc:\n        other: \"Toto bylo zveřejněno jako odpověď, ale nesnaží se odpovědět na dotaz. Měla by to být úprava, komentář, nebo úplně jiný dotaz.\"\n    no_longer_needed:\n      name:\n        other: již není potřeba\n      desc:\n        other: Tento komentář je zastaralý, konverzační nebo není relevantní pro tento příspěvek.\n    something:\n      name:\n        other: jiný důvod\n      desc:\n        other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše.\n      placeholder:\n        other: Dejte nám vědět konkrétně, v čem je problém\n    community_specific:\n      name:\n        other: důvod specifický pro komunitu\n      desc:\n        other: Tento dotaz nesplňuje pravidla komunity.\n    not_clarity:\n      name:\n        other: vyžaduje detaily nebo upřesnění\n      desc:\n        other: Tento dotaz v současné době obsahuje více otázek. Měl by se zaměřit pouze na jeden problém.\n    looks_ok:\n      name:\n        other: vypadá v pořádku\n      desc:\n        other: Tento příspěvek je dobrý tak jak je, nemá nízkou kvalitu.\n    needs_edit:\n      name:\n        other: potřebuje úpravu, kterou jsem udělal(a)\n      desc:\n        other: Zlepšete a opravte problémy s tímto příspěvkem.\n    needs_close:\n      name:\n        other: potřebuje zavřít\n      desc:\n        other: Na uzavřený dotaz není možné odpovídat, ale stále může být upraven a je možné pro něj hlasovat a komentovat jej.\n    needs_delete:\n      name:\n        other: potřebuje smazat\n      desc:\n        other: Tento příspěvek bude odstraněn.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Tento dotaz byl položena dříve a již má odpověď.\n      guideline:\n        name:\n          other: důvod specifický pro komunitu\n        desc:\n          other: Tento dotaz nesplňuje pravidla komunity.\n      multiple:\n        name:\n          other: vyžaduje detaily nebo upřesnění\n        desc:\n          other: Tento dotaz v současné době obsahuje více otázek. Měla by se zaměřit pouze na jeden problém.\n      other:\n        name:\n          other: jiný důvod\n        desc:\n          other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše.\n    operation_type:\n      asked:\n        other: dotázáno\n      answered:\n        other: zodpovězeno\n      modified:\n        other: upraveno\n    deleted_title:\n      other: Smazat dotaz\n    questions_title:\n      other: Dotazy\n  tag:\n    tags_title:\n      other: Štítky\n    no_description:\n      other: Štítek nemá žádný popis.\n  notification:\n    action:\n      update_question:\n        other: upravený dotaz\n      answer_the_question:\n        other: položil(a) dotaz\n      update_answer:\n        other: upravil(a) odpověď\n      accept_answer:\n        other: přijal(a) odpověď\n      comment_question:\n        other: okomentoval(a) dotaz\n      comment_answer:\n        other: okomentoval(a) odpověď\n      reply_to_you:\n        other: vám odpověděl(a)\n      mention_you:\n        other: vás zmínil(a)\n      your_question_is_closed:\n        other: Váš dotaz byl uzavřen\n      your_question_was_deleted:\n        other: Váš dotaz byl odstraněn\n      your_answer_was_deleted:\n        other: Vaše odpověď byla smazána\n      your_comment_was_deleted:\n        other: Váš komentář byl odstraněn\n      up_voted_question:\n        other: hlasoval(a) pro dotaz\n      down_voted_question:\n        other: hlasoval(a) proti dotazu\n      up_voted_answer:\n        other: hlasoval(a) pro odpověď\n      down_voted_answer:\n        other: hlasoval(a) proti odpovědi\n      up_voted_comment:\n        other: hlasoval(a) pro komentář\n      invited_you_to_answer:\n        other: vás pozval, abyste odpověděl(a)\n      earned_badge:\n        other: Získali jste odznak \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Potvrďte svůj nový email\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} odpověděl(a) na váš dotaz\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Obnova hesla\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Potvrďte svůj nový účet\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Zkušební email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: hlasovat pro\n    upvoted:\n      other: hlasováno pro\n    downvote:\n      other: hlasovat proti\n    downvoted:\n      other: hlasováno proti\n    accept:\n      other: přijmout\n    accepted:\n      other: přijato\n    edit:\n      other: upravit\n  review:\n    queued_post:\n      other: Příspěvek ve frontě\n    flagged_post:\n      other: Nahlášený příspěvek\n    suggested_post_edit:\n      other: Navrhované úpravy\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} a {{ .Count }} dalších...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Životopisec\n        desc:\n          other: <a href=\"{{ .ProfileURL }}\" target=\"_blank\">Profil</a> vyplněn.\n      certified:\n        name:\n          other: Certifikovaný\n        desc:\n          other: Tutoriál pro nové uživatele dokončen.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: První úprava příspěvku.\n      first_flag:\n        name:\n          other: První nahlášení\n        desc:\n          other: První nahlášení příspěvku.\n      first_upvote:\n        name:\n          other: První hlas pro\n        desc:\n          other: První hlas pro příspěvek.\n      first_link:\n        name:\n          other: První odkaz\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: První sdílení\n        desc:\n          other: První sdílení příspěvku.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Napište 5 komentářů.\n      new_user_of_the_month:\n        name:\n          other: Nový uživatel měsíce\n        desc:\n          other: Výjimečný přínos ve svém prvním měsíci na stránce.\n      read_guidelines:\n        name:\n          other: Přečíst pravidla\n        desc:\n          other: Přečtěte si [pravidla komunity].\n      reader:\n        name:\n          other: Čtenář\n        desc:\n          other: Přečtěte si všechny odpovědi v tématu s více než 10 odpověďmi.\n      welcome:\n        name:\n          other: Vítejte\n        desc:\n          other: Obdržel(a) hlas.\n      nice_share:\n        name:\n          other: Povedené sdílení\n        desc:\n          other: Sdílel(a) příspěvek s 25 unikátními návštěvníky.\n      good_share:\n        name:\n          other: Dobré sdílení\n        desc:\n          other: Sdílel(a) příspěvek s 300 unikátními návštěvníky.\n      great_share:\n        name:\n          other: Skvělé sdílení\n        desc:\n          other: Sdílel(a) příspěvek s 1000 unikátními návštěvníky.\n      out_of_love:\n        name:\n          other: Optimista\n        desc:\n          other: Využito 50 hlasů pro za den.\n      higher_love:\n        name:\n          other: Vytrvalý optimista\n        desc:\n          other: 5 krát využito 50 hlasů pro za den.\n      crazy_in_love:\n        name:\n          other: Bláznivý optimista\n        desc:\n          other: 20 krát využito 50 hlasů pro za den.\n      promoter:\n        name:\n          other: Promotér\n        desc:\n          other: Pozval(a) uživatele.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Pozval(a) 3 uživatele.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Edit Tag\n    ask_a_question: Create Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    new_alerts: New alerts\n    all_read: Mark all as read\n    show_more: Show more\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Pozvěte další uživatele\n    desc: Pozvěte lidi, o kterých si myslíte, že mohou odpovědět.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Každý uživatel může napsat pouze jednu odpověď na stejný dotaz\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: URL základny Gravatar\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/cy_GB.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Llwyddiant.\n    unknown:\n      other: Gwall anhysbys.\n    request_format_error:\n      other: Nid yw fformat y cais yn ddilys.\n    unauthorized_error:\n      other: Anawdurdodedig.\n    database_error:\n      other: Gwall gweinydd data.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Tynnu sylw\n    edit:\n      other: Golygu\n    delete:\n      other: Dileu\n    close:\n      other: Cau\n    reopen:\n      other: Ailagor\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pinio\n    hide:\n      other: Dad-restru\n    unpin:\n      other: Dadbinio\n    show:\n      other: Rhestr\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Defnyddiwr\n      admin:\n        other: Gweinyddwr\n      moderator:\n        other: Cymedrolwr\n    description:\n      user:\n        other: Diofyn heb unrhyw fynediad arbennig.\n      admin:\n        other: Bod â'r pŵer llawn i gael mynediad i'r safle.\n      moderator:\n        other: Mae ganddo fynediad i bob post ac eithrio gosodiadau gweinyddol.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Ebost\n  e_mail:\n    other: Email\n  password:\n    other: Cyfrinair\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: Nid yw e-bost a chyfrinair yn cyfateb.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: Ni allwch addasu eich cyfrinair.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: Ni allwch addasu eich statws.\n      email_or_password_wrong:\n        other: Nid yw e-bost a chyfrinair yn cyfateb.\n    answer:\n      not_found:\n        other: Ni cheir yr ateb.\n      cannot_deleted:\n        other: Dim caniatâd i ddileu.\n      cannot_update:\n        other: Dim caniatâd i ddiweddaru.\n      question_closed_cannot_add:\n        other: Mae cwestiynau ar gau ac ni ellir eu hychwanegu.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Nid oes modd golygu sylwadau.\n      not_found:\n        other: Sylw heb ei ganfod.\n      cannot_edit_after_deadline:\n        other: Mae'r amser sylwadau wedi bod yn rhy hir i'w addasu.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: E-bost yn bodoli eisoes.\n      need_to_be_verified:\n        other: Dylid gwirio e-bost.\n      verify_url_expired:\n        other: Mae'r URL wedi'i wirio gan e-bost wedi dod i ben, anfonwch yr e-bost eto.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Ffeil iaith heb ei chanfod.\n    object:\n      captcha_verification_failed:\n        other: Captcha anghywir.\n      disallow_follow:\n        other: Ni chaniateir i chi ddilyn.\n      disallow_vote:\n        other: Ni chaniateir i chi pleidleisio.\n      disallow_vote_your_self:\n        other: Ni allwch bleidleisio dros eich post eich hun.\n      not_found:\n        other: Heb ganfod y gwrthrych.\n      verification_failed:\n        other: Methodd y dilysu.\n      email_or_password_incorrect:\n        other: Nid yw e-bost a chyfrinair yn cyfateb.\n      old_password_verification_failed:\n        other: Methodd yr hen ddilysiad cyfrinair\n      new_password_same_as_previous_setting:\n        other: Mae'r cyfrinair newydd yr un fath â'r un blaenorol.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: Mae'r postiad hwn wedi'i ddileu.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Cwestiwn heb ei ganfod.\n      cannot_deleted:\n        other: Dim caniatâd i ddileu.\n      cannot_close:\n        other: Dim caniatâd i cau.\n      cannot_update:\n        other: Dim caniatâd i ddiweddaru.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Methodd handlen yr adroddiad.\n      not_found:\n        other: Heb ganfod yr adroddiad.\n    tag:\n      already_exist:\n        other: Mae tag eisoes yn bodoli.\n      not_found:\n        other: Tag heb ei ddarganfod.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Rhowch o leiaf un tag gofynnol.\n      not_contain_synonym_tags:\n        other: Ni ddylai gynnwys tagiau cyfystyr.\n      cannot_update:\n        other: Dim caniatâd i ddiweddaru.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: Ni allwch osod cyfystyr y tag cyfredol fel ei hun.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Thema heb ei ddarganfod.\n    revision:\n      review_underway:\n        other: Methu â golygu ar hyn o bryd, mae fersiwn yn y ciw adolygu.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Nid yw e-bost a chyfrinair yn cyfateb.\n      not_found:\n        other: Defnyddwr heb ei ddarganfod.\n      suspended:\n        other: Mae'r defnyddiwr hwn wedi'i atal.\n      username_invalid:\n        other: Mae'r enw defnyddiwr yn annilys.\n      username_duplicate:\n        other: Cymerwyd yr enw defnyddiwr eisoes.\n      set_avatar:\n        other: Methodd set avatar.\n      cannot_update_your_role:\n        other: Ni allwch addasu eich rôl.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Wedi methu darllen y ffurfwedd\n    database:\n      connection_failed:\n        other: Methodd cysylltiad cronfa ddata\n      create_table_failed:\n        other: Methwyd creu tabl\n    install:\n      create_config_failed:\n        other: Methu creu'r ffeil config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Fformat ffeil heb ei gefnogi.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: sbam\n        desc:\n          other: Mae'r cwestiwn hwn wedi'i ofyn o'r blaen ac mae ganddo ateb yn barod.\n      guideline:\n        name:\n          other: rheswm cymunedol-benodol\n        desc:\n          other: Nid yw'r cwestiwn hwn yn bodloni canllaw cymunedol.\n      multiple:\n        name:\n          other: angen manylion neu eglurder\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: rhywbeth arall\n        desc:\n          other: Mae'r swydd hon angen reswm arall nad yw wedi'i restru uchod.\n    operation_type:\n      asked:\n        other: gofynnodd\n      answered:\n        other: atebodd\n      modified:\n        other: wedi newid\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: cwestiwn wedi'i ddiweddaru\n      answer_the_question:\n        other: cwestiwn wedi ei ateb\n      update_answer:\n        other: ateb wedi'i ddiweddaru\n      accept_answer:\n        other: ateb derbyniol\n      comment_question:\n        other: cwestiwn a wnaed\n      comment_answer:\n        other: ateb a wnaed\n      reply_to_you:\n        other: atebodd i chi\n      mention_you:\n        other: wedi sôn amdanoch\n      your_question_is_closed:\n        other: Mae eich cwestiwn wedi’i gau\n      your_question_was_deleted:\n        other: Mae eich cwestiwn wedi’i dileu\n      your_answer_was_deleted:\n        other: Mae eich ateb wedi’i dileu\n      your_comment_was_deleted:\n        other: Mae eich sylw wedi’i dileu\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Sut i Fformatio\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Cynt\n    next: Nesaf\n  page_title:\n    question: Cwestiwn\n    questions: Cwestiynau\n    tag: Tag\n    tags: Tagiau\n    tag_wiki: tag wiki\n    create_tag: Creu Tag\n    edit_tag: Golygu Tag\n    ask_a_question: Create Question\n    edit_question: Golygu Cwestiwn\n    edit_answer: Golygu Ateb\n    search: Chwiliwch\n    posts_containing: Postiadau yn cynnwys\n    settings: Gosodiadau\n    notifications: Hysbysiadau\n    login: Mewngofnodi\n    sign_up: Cofrestru\n    account_recovery: Adfer Cyfrif\n    account_activation: Ysgogi Cyfrif\n    confirm_email: Cadarnhau e-bost\n    account_suspended: Cyfrif wedi'i atal\n    admin: Gweinyddu\n    change_email: Addasu E-bost\n    install: Ateb Gosod\n    upgrade: Ateb Uwchraddio\n    maintenance: Cynnal a Chadw Gwefan\n    users: Defnyddwyr\n    oauth_callback: Processing\n    http_404: Gwall HTTP 404\n    http_50X: Gwall HTTP 500\n    http_403: Gwall HTTP 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Hysbysiadau\n    inbox: Mewnflwch\n    achievement: Llwyddiannau\n    new_alerts: New alerts\n    all_read: Marciwch y cyfan fel wedi'i ddarllen\n    show_more: Dangos mwy\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Mae'ch Cyfrif wedi'i Atal\n    until_time: \"Cafodd eich cyfrif ei atal tan {{ time }}.\"\n    forever: Cafodd y defnyddiwr hwn ei atal am byth.\n    end: Nid ydych yn arwain cymunedol.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Dyfyniad\n    bold:\n      text: Cryf\n    chart:\n      text: Siart\n      flow_chart: Siart llif\n      sequence_diagram: Diagram dilyniant\n      class_diagram: Diagram dosbarth\n      state_diagram: Diagram cyflwr\n      entity_relationship_diagram: Diagram perthynas endid\n      user_defined_diagram: Diagram wedi'i ddiffinio gan y defnyddiwr\n      gantt_chart: Siart Gantt\n      pie_chart: Siart cylch\n    code:\n      text: Sampl côd\n      add_code: Ychwanegu sampl côd\n      form:\n        fields:\n          code:\n            label: Côd\n            msg:\n              empty: Ni all côd fod yn wag.\n          language:\n            label: Iaith\n            placeholder: Synhwyriad awtomatig\n      btn_cancel: Canslo\n      btn_confirm: Ychwanegu\n    formula:\n      text: Fformiwla\n      options:\n        inline: Fformiwla mewn-lein\n        block: Fformiwla bloc\n    heading:\n      text: Pennawd\n      options:\n        h1: Pennawd 1\n        h2: Pennawd 2\n        h3: Pennawd 3\n        h4: Pennawd 4\n        h5: Pennawd 5\n        h6: Pennawd 6\n    help:\n      text: Cymorth\n    hr:\n      text: Horizontal rule\n    image:\n      text: Delwedd\n      add_image: Ychwanegu delwedd\n      tab_image: Uwchlwytho delwedd\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Dewis delwedd\n            msg:\n              empty: Ni all ffeil fod yn wag.\n              only_image: Dim ond ffeiliau delwedd a ganiateir.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Disgrifiad\n      tab_url: URL delwedd\n      form_url:\n        fields:\n          url:\n            label: URL delwedd\n            msg:\n              empty: Ni all URL delwedd fod yn wag.\n          name:\n            label: Disgrifiad\n      btn_cancel: Canslo\n      btn_confirm: Ychwanegu\n      uploading: Wrthi'n uwchlwytho\n    indent:\n      text: Mewnoliad\n    outdent:\n      text: Alloliad\n    italic:\n      text: Pwyslais\n    link:\n      text: Hypergyswllt\n      add_link: Ychwanegu hypergyswllt\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: Ni all URL fod yn wag.\n          name:\n            label: Disgrifiad\n      btn_cancel: Canslo\n      btn_confirm: Ychwanegu\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Tabl\n      heading: Pennawd\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Rwy'n cau'r post hon fel...\n    btn_cancel: Canslo\n    btn_submit: Cyflwyno\n    remark:\n      empty: Ni all fod yn wag.\n    msg:\n      empty: Dewis rheswm.\n  report_modal:\n    flag_title: Dwi'n tynnu sylw i adrodd y swydd hon fel...\n    close_title: Rwy'n cau'r post hon fel...\n    review_question_title: Adolygu cwestiwn\n    review_answer_title: Adolygu ateb\n    review_comment_title: Adolygu sylwad\n    btn_cancel: Canslo\n    btn_submit: Cyflwyno\n    remark:\n      empty: Ni all fod yn wag.\n    msg:\n      empty: Dewis rheswm.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Creu tag newydd\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Ni all fod enw dangos yn wag.\n            range: Enw arddangos hyd at 35 nod.\n        slug_name:\n          label: URL slug\n          desc: Slug URL hyd at 35 nod.\n          msg:\n            empty: Ni all Slug URL fod yn wag.\n            range: Slug URL hyd at 35 nod.\n            character: Mae slug URL yn cynnwys set nodau na caniateir.\n        desc:\n          label: Disgrifiad\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Canslo\n    btn_submit: Cyflwyno\n    btn_post: Post tag newydd\n  tag_info:\n    created_at: Creuwyd\n    edited_at: Golygwyd\n    history: Hanes\n    synonyms:\n      title: Cyfystyron\n      text: Bydd y tagiau canlynol yn cael eu hail-fapio i\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Canslo\n  tags:\n    title: Tagiau\n    sort_buttons:\n      popular: Poblogaidd\n      name: Enw\n      newest: Newest\n    button_follow: Dilyn\n    button_following: Yn dilyn\n    tag_label: cwestiynau\n    search_placeholder: Hidlo yn ôl enw tag\n    no_desc: Nid oes gan y tag unrhyw ddisgrifiad.\n    more: Mwy\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Golygu Cwestiwn\n    default_reason: Golygu Cwestiwn\n    default_first_reason: Create question\n    similar_questions: Cwestiynau tebyg\n    form:\n      fields:\n        revision:\n          label: Diwygiad\n        title:\n          label: Teitl\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Ni all teitl fod yn wag.\n            range: Teitl hyd at 20 nod\n        body:\n          label: Corff\n          msg:\n            empty: Ni all corff fod yn wag.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tagiau\n          msg:\n            empty: Ni all tagiau fod yn wag.\n        answer:\n          label: Ateb\n          msg:\n            empty: Ni all ateb fod yn wag.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Eglurwch yn fyr eich newidiadau (sillafu wedi'i gywiro, gramadeg sefydlog, fformatio gwell)\n    btn_post_question: Post cweistiwn\n    btn_save_edits: Cadw golygiadau\n    answer_question: Atebwch eich cwestiwn eich hun\n    post_question&answer: Postiwch eich cwestiwn ac ateb\n  tag_selector:\n    add_btn: Ychwanegu tag\n    create_btn: Creu tag newydd\n    search_tag: Chwilio tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Select people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/da_DK.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Gennemført.\n    unknown:\n      other: Ukendt fejl.\n    request_format_error:\n      other: Forespørgselsformat er ikke gyldigt.\n    unauthorized_error:\n      other: Uautoriseret.\n    database_error:\n      other: Data-server fejl.\n    forbidden_error:\n      other: Forbudt.\n    duplicate_request_error:\n      other: Duplilkeret indenselse.\n  action:\n    report:\n      other: Anmeld\n    edit:\n      other: Rediger\n    delete:\n      other: Slet\n    close:\n      other: Luk\n    reopen:\n      other: Genåbn\n    forbidden_error:\n      other: Forbudt.\n    pin:\n      other: Fastgør\n    hide:\n      other: Afliste\n    unpin:\n      other: Frigør\n    show:\n      other: Liste\n    invite_someone_to_answer:\n      other: Rediger\n    undelete:\n      other: Genopret\n    merge:\n      other: Sammenflet\n  role:\n    name:\n      user:\n        other: Bruger\n      admin:\n        other: Administrator\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Standard uden særlig adgang.\n      admin:\n        other: Hav den fulde magt til at få adgang til webstedet.\n      moderator:\n        other: Har adgang til alle opslag undtagen administratorindstillinger.\n  privilege:\n    level_1:\n      description:\n        other: Niveau 1 (mindre omdømme kræves for private team, gruppe)\n    level_2:\n      description:\n        other: Niveau 2 (lav omdømme kræves for opstart fællesskab)\n    level_3:\n      description:\n        other: Niveau 3 (højt omdømme kræves for moden fællesskab)\n    level_custom:\n      description:\n        other: Brugerdefineret Niveau\n    rank_question_add_label:\n      other: Stil spørgsmål\n    rank_answer_add_label:\n      other: Skriv svar\n    rank_comment_add_label:\n      other: Skriv kommentar\n    rank_report_add_label:\n      other: Anmeld\n    rank_comment_vote_up_label:\n      other: Op-stem kommentar\n    rank_link_url_limit_label:\n      other: Oplæg mere end 2 links ad gangen\n    rank_question_vote_up_label:\n      other: Op-stem spørgsmål\n    rank_answer_vote_up_label:\n      other: Op-stem svar\n    rank_question_vote_down_label:\n      other: Ned-stem spørgsmål\n    rank_answer_vote_down_label:\n      other: Ned-stem svar\n    rank_invite_someone_to_answer_label:\n      other: Inviter nogen til at svare\n    rank_tag_add_label:\n      other: Opret et nyt nøgleord\n    rank_tag_edit_label:\n      other: Rediger nøgleord beskrivelse (skal gennemgås)\n    rank_question_edit_label:\n      other: Rediger andres spørgsmål (skal gennemgås)\n    rank_answer_edit_label:\n      other: Redigere andres svar (skal gennemgås)\n    rank_question_edit_without_review_label:\n      other: Rediger andres spørgsmål uden gennemgang\n    rank_answer_edit_without_review_label:\n      other: Rediger andres svar uden gennemgang\n    rank_question_audit_label:\n      other: Gennemse spørgsmål redigeringer\n    rank_answer_audit_label:\n      other: Gennemgå svar redigeringer\n    rank_tag_audit_label:\n      other: Gennemse nøgleord redigeringer\n    rank_tag_edit_without_review_label:\n      other: Rediger nøgleord beskrivelse uden gennemgang\n    rank_tag_synonym_label:\n      other: Administrer nøgleord synonymer\n  email:\n    other: E-mail\n  e_mail:\n    other: E-mail\n  password:\n    other: Adgangskode\n  pass:\n    other: Adgangskode\n  old_pass:\n    other: Nuværende adgangskode\n  original_text:\n    other: Dette opslag\n  email_or_password_wrong_error:\n    other: E-mail og adgangskode stemmer ikke overens.\n  error:\n    common:\n      invalid_url:\n        other: Ugyldig URL.\n      status_invalid:\n        other: Ugyldig status.\n    password:\n      space_invalid:\n        other: Adgangskoden må ikke indeholde mellemrum.\n    admin:\n      cannot_update_their_password:\n        other: Du kan ikke ændre din adgangskode.\n      cannot_edit_their_profile:\n        other: Du kan ikke ændre din profil.\n      cannot_modify_self_status:\n        other: Du kan ikke ændre din status.\n      email_or_password_wrong:\n        other: E-mail og adgangskode stemmer ikke overens.\n    answer:\n      not_found:\n        other: Svar ikke fundet.\n      cannot_deleted:\n        other: Ingen tilladelser til at slette.\n      cannot_update:\n        other: Ingen tilladelse til at opdatere.\n      question_closed_cannot_add:\n        other: Spørgsmål er lukket og kan ikke tilføjes.\n      content_cannot_empty:\n        other: Svar skal udfyldes.\n    comment:\n      edit_without_permission:\n        other: Kommentar er ikke tilladt at redigere.\n      not_found:\n        other: Kommentar ikke fundet.\n      cannot_edit_after_deadline:\n        other: Kommentaren er for gammel til at blive redigeret.\n      content_cannot_empty:\n        other: Kommentar indhold kan ikke være tomt.\n    email:\n      duplicate:\n        other: Email eksisterer allerede.\n      need_to_be_verified:\n        other: E-mail skal bekræftes.\n      verify_url_expired:\n        other: Email bekræftet URL er udløbet. Send venligst e-mailen igen.\n      illegal_email_domain_error:\n        other: E-mail er ikke tilladt fra dette e-mail-domæne. Brug venligst et andet.\n    lang:\n      not_found:\n        other: Sprog-fil kunne ikke findes.\n    object:\n      captcha_verification_failed:\n        other: Captcha er forkert.\n      disallow_follow:\n        other: Du har ikke tilladelse til at følge.\n      disallow_vote:\n        other: Du har ikke tilladelse til at stemme.\n      disallow_vote_your_self:\n        other: Du kan ikke stemme på dit eget indlæg.\n      not_found:\n        other: Objekt ikke fundet.\n      verification_failed:\n        other: Verifikation mislykkedes.\n      email_or_password_incorrect:\n        other: E-mail og adgangskode stemmer ikke overens.\n      old_password_verification_failed:\n        other: Den gamle adgangskodebekræftelse mislykkedes\n      new_password_same_as_previous_setting:\n        other: Den nye adgangskode er den samme som den foregående.\n      already_deleted:\n        other: Dette indlæg er blevet slettet.\n    meta:\n      object_not_found:\n        other: Metaobjekt ikke fundet\n    question:\n      already_deleted:\n        other: Dette indlæg er blevet slettet.\n      under_review:\n        other: Dit indlæg afventer gennemgang. Det vil være synligt, når det er blevet godkendt.\n      not_found:\n        other: Spørgsmål ikke fundet.\n      cannot_deleted:\n        other: Ingen tilladelser til at slette.\n      cannot_close:\n        other: Ingen tilladelse til at lukke.\n      cannot_update:\n        other: Ingen tilladelse til at opdatere.\n      content_cannot_empty:\n        other: Indhold kan ikke være tomt.\n      content_less_than_minimum:\n        other: Ikke nok indhold indtastet.\n    rank:\n      fail_to_meet_the_condition:\n        other: Omdømmelse rang opfylder ikke betingelsen.\n      vote_fail_to_meet_the_condition:\n        other: Tak for feedback. Du skal mindst have {{.Rank}} ry for at afgive en stemme.\n      no_enough_rank_to_operate:\n        other: Du skal mindst {{.Rank}} omdømme for at gøre dette.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Rapport ikke fundet.\n    tag:\n      already_exist:\n        other: Nøgleord findes allerede.\n      not_found:\n        other: Nøgleord blev ikke fundet.\n      recommend_tag_not_found:\n        other: Anbefal nøgleord eksisterer ikke.\n      recommend_tag_enter:\n        other: Indtast mindst et påkrævet nøgleord.\n      not_contain_synonym_tags:\n        other: Må ikke indeholde synonym nøgleord.\n      cannot_update:\n        other: Ingen tilladelse til at opdatere.\n      is_used_cannot_delete:\n        other: Du kan ikke slette et nøgleord, der er i brug.\n      cannot_set_synonym_as_itself:\n        other: Du kan ikke indstille synonymet for det nuværende nøgleord som sig selv.\n      minimum_count:\n        other: Ikke nok nøgleord blev indtastet.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Fra-navnet kan ikke være en e-mail-adresse.\n    theme:\n      not_found:\n        other: Tema ikke fundet.\n    revision:\n      review_underway:\n        other: Kan ikke redigere i øjeblikket, der er en version i revisionskøen.\n      no_permission:\n        other: Ingen tilladelse til at revidere.\n    user:\n      external_login_missing_user_id:\n        other: Den tredjepartsplatform giver ikke et unikt UserID, så du kan ikke logge ind, kontakt venligst webstedsadministratoren.\n      external_login_unbinding_forbidden:\n        other: Angiv en adgangskode til din konto, før du fjerner dette login.\n      email_or_password_wrong:\n        other:\n          other: E-mail og adgangskode stemmer ikke overens.\n      not_found:\n        other: Bruger ikke fundet.\n      suspended:\n        other: Brugeren er suspenderet.\n      username_invalid:\n        other: Brugernavn er ugyldigt.\n      username_duplicate:\n        other: Brugernavn er allerede i brug.\n      set_avatar:\n        other: Avatar sæt mislykkedes.\n      cannot_update_your_role:\n        other: Du kan ikke ændre din rolle.\n      not_allowed_registration:\n        other: Webstedet er ikke åbent for registrering.\n      not_allowed_login_via_password:\n        other: I øjeblikket er det ikke tilladt at logge ind via adgangskode.\n      access_denied:\n        other: Adgang nægtet\n      page_access_denied:\n        other: Du har ikke adgang til denne side.\n      add_bulk_users_format_error:\n        other: \"Fejl {{.Field}} format nær '{{.Content}}' i linje {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Antallet af brugere du tilføjer på én gang skal være i intervallet 1 -{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>Denne bruger blev suspenderet for evigt.</strong> Denne bruger opfylder ikke en fællesskabsretningslinje.\"\n      status_suspended_until:\n        other: \"<strong>Denne bruger blev suspenderet indtil {{.SuspendedUntil}}.</strong> Denne bruger opfylder ikke en fællesskabsretningslinje.\"\n      status_deleted:\n        other: \"Denne bruger blev slettet.\"\n      status_inactive:\n        other: \"Denne bruger er ikke aktiv.\"\n    config:\n      read_config_failed:\n        other: Kunne ikke læse konfigurationen\n    database:\n      connection_failed:\n        other: Database forbindelse mislykkedes\n      create_table_failed:\n        other: Tabellen kunne ikke oprettes\n    install:\n      create_config_failed:\n        other: Kan ikke oprette filen config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Ikke understøttet filformat.\n    site_info:\n      config_not_found:\n        other: Site config ikke fundet.\n    badge:\n      object_not_found:\n        other: Emblem objekt ikke fundet\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Dette indlæg er en annonce eller vandalisme. Det er ikke nyttigt eller relevant for det aktuelle emne.\n    rude_or_abusive:\n      name:\n        other: uhøflig eller misbrug\n      desc:\n        other: \"En fornuftig person ville finde dette indhold upassende eller ikke respektfuldt.\"\n    a_duplicate:\n      name:\n        other: en duplikering\n      desc:\n        other: Dette spørgsmål er blevet stillet før og har allerede et svar.\n      placeholder:\n        other: Indtast linket til eksisterende spørgsmål\n    not_a_answer:\n      name:\n        other: ikke et svar\n      desc:\n        other: \"Dette blev sendt som svar, men det forsøger ikke at besvare spørgsmålet. Det bør muligvis være en redigering, en kommentar, et andet spørgsmål, eller slettet helt.\"\n    no_longer_needed:\n      name:\n        other: ikke længere nødvendigt\n      desc:\n        other: Denne kommentar er forældet, samtale-agtig eller ikke relevant for dette indlæg.\n    something:\n      name:\n        other: noget andet\n      desc:\n        other: Dette indlæg kræver personalets opmærksomhed af en anden grund, som ikke er nævnt ovenfor.\n      placeholder:\n        other: Lad os vide specifikt, hvad du er bekymret over\n    community_specific:\n      name:\n        other: en fællesskabsspecifik årsag\n      desc:\n        other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje.\n    not_clarity:\n      name:\n        other: kræver detaljer eller klarhed\n      desc:\n        other: Dette spørgsmål indeholder i øjeblikket flere spørgsmål i én. Det bør kun fokusere på ét problem.\n    looks_ok:\n      name:\n        other: ser OK ud\n      desc:\n        other: Dette indlæg er godt som er og ikke lav kvalitet.\n    needs_edit:\n      name:\n        other: har brug for redigering, og jeg gjorde det\n      desc:\n        other: Forbedre og ret selv problemer med dette indlæg.\n    needs_close:\n      name:\n        other: skal lukkes\n      desc:\n        other: Et lukket spørgsmål kan ikke besvares, men du kan stadig redigere, stemme og kommentere.\n    needs_delete:\n      name:\n        other: skal slettes\n      desc:\n        other: Dette indlæg bliver slettet.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Dette spørgsmål er blevet stillet før og har allerede et svar.\n      guideline:\n        name:\n          other: en fællesskabsspecifik årsag\n        desc:\n          other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje.\n      multiple:\n        name:\n          other: kræver detaljer eller klarhed\n        desc:\n          other: Dette spørgsmål indeholder i øjeblikket flere spørgsmål i én. Det bør kun fokusere på ét problem.\n      other:\n        name:\n          other: noget andet\n        desc:\n          other: Dette indlæg kræver en anden grund som ikke er nævnt ovenfor.\n    operation_type:\n      asked:\n        other: spurgt\n      answered:\n        other: besvaret\n      modified:\n        other: ændret\n    deleted_title:\n      other: Slettet spørgsmål\n    questions_title:\n      other: Spørgsmål\n  tag:\n    tags_title:\n      other: Nøgleord\n    no_description:\n      other: Nøgleord har ingen beskrivelse.\n  notification:\n    action:\n      update_question:\n        other: opdateret spørgsmål\n      answer_the_question:\n        other: besvaret spørgsmål\n      update_answer:\n        other: opdateret svar\n      accept_answer:\n        other: accepteret svar\n      comment_question:\n        other: kommenteret spørgsmål\n      comment_answer:\n        other: kommenteret svar\n      reply_to_you:\n        other: svarede dig\n      mention_you:\n        other: nævnte dig\n      your_question_is_closed:\n        other: Dit spørgsmål er blevet lukket\n      your_question_was_deleted:\n        other: Dit spørgsmål er blevet slettet\n      your_answer_was_deleted:\n        other: Dit svar er blevet slettet\n      your_comment_was_deleted:\n        other: Din kommentar er slettet\n      up_voted_question:\n        other: op-stemt spørgsmål\n      down_voted_question:\n        other: ned-stemt spørgsmål\n      up_voted_answer:\n        other: op-stemt svar\n      down_voted_answer:\n        other: ned-stemt svar\n      up_voted_comment:\n        other: op-stemt kommentar\n      invited_you_to_answer:\n        other: inviterede dig til at svare\n      earned_badge:\n        other: Du har optjent \"{{.BadgeName}}\" emblem\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Bekræft din nye e-mailadresse\"\n      body:\n        other: \"Bekræft din nye e-mailadresse for {{.SiteName}} ved at klikke på følgende link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nHvis du ikke anmodede om denne ændring, ignorér venligst denne e-mail.<br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk systeme-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} besvarede dit spørgsmål\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Se den på {{.SiteName}}</a><br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Afmeld</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} inviterede dig til at svare\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Jeg tror du måske kender svaret.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Se den på {{.SiteName}}</a><br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk systeme-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Afmeld</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} kommenterede dit indlæg\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Se den på {{.SiteName}}</a><br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Afmeld</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nyt spørgsmål: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Afmeld</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Nulstilling af adgangskode\"\n      body:\n        other: \"Nogen bad om at nulstille din adgangskode på {{.SiteName}}.<br><br>\\n\\nHvis det ikke var dig, kan du trygt ignorere denne e-mail.<br><br>\\n\\nKlik på følgende link for at vælge en ny adgangskode:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail, svar venligst ikke på denne besked, da dit svar ikke vil blive set.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Bekræft din nye konto\"\n      body:\n        other: \"Velkommen til {{.SiteName}}!<br><br>\\n\\nKlik på følgende link for at bekræfte og aktivere din nye konto:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nHvis ovenstående link ikke kan klikkes på, prøv at kopiere og indsætte det i adresselinjen i din webbrowser.\\n<br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail svar venligst ikke på denne besked, da dit svar ikke vil blive set.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test E-Mail\"\n      body:\n        other: \"Dette er en test e-mail.\\n<br><br>\\n\\n--<br>\\nBemærk: Dette er en automatisk system-mail svar venligst ikke på denne besked, da dit svar ikke vil blive set.\"\n  action_activity_type:\n    upvote:\n      other: stem op\n    upvoted:\n      other: stemt op\n    downvote:\n      other: stem ned\n    downvoted:\n      other: stemt ned\n    accept:\n      other: acceptér\n    accepted:\n      other: accepteret\n    edit:\n      other: rediger\n  review:\n    queued_post:\n      other: Indlæg i kø\n    flagged_post:\n      other: Anmeldt indlæg\n    suggested_post_edit:\n      other: Foreslåede redigeringer\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} og {{ .Count }} mere...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiografer\n        desc:\n          other: Udfyldte <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profil</a> information.\n      certified:\n        name:\n          other: Certificeret\n        desc:\n          other: Færdiggjorde vores nye brugervejledning.\n      editor:\n        name:\n          other: Redaktør\n        desc:\n          other: Første indlæg redigeret.\n      first_flag:\n        name:\n          other: Første Markering\n        desc:\n          other: Først markerede et indlæg.\n      first_upvote:\n        name:\n          other: Første Opstemme\n        desc:\n          other: Først stemte et indlæg op.\n      first_link:\n        name:\n          other: Første Link\n        desc:\n          other: Først tilføjede et link til et andet indlæg.\n      first_reaction:\n        name:\n          other: Første Reaktion\n        desc:\n          other: Først reagerede på indlægget.\n      first_share:\n        name:\n          other: Første Deling\n        desc:\n          other: Først delte et indlæg.\n      scholar:\n        name:\n          other: Elev\n        desc:\n          other: Stillede et spørgsmål og accepterede et svar.\n      commentator:\n        name:\n          other: Kommentator\n        desc:\n          other: Efterlad 5 kommentarer.\n      new_user_of_the_month:\n        name:\n          other: Månedens nye bruger\n        desc:\n          other: Fremragende bidrag i deres første måned.\n      read_guidelines:\n        name:\n          other: Læs Retningslinjer\n        desc:\n          other: Læs [fællesskabsretningslinjerne].\n      reader:\n        name:\n          other: Læser\n        desc:\n          other: Læs alle svar i et emne med mere end 10 svar.\n      welcome:\n        name:\n          other: Velkommen\n        desc:\n          other: Modtog en op-stemme.\n      nice_share:\n        name:\n          other: Dejlig Deling\n        desc:\n          other: Delte et indlæg med 25 unikke besøgende.\n      good_share:\n        name:\n          other: God Deling\n        desc:\n          other: Delte et indlæg med 300 unikke besøgende.\n      great_share:\n        name:\n          other: Fantastisk Deling\n        desc:\n          other: Delte et indlæg med 1000 unikke besøgende.\n      out_of_love:\n        name:\n          other: Af kærlighed\n        desc:\n          other: Brugte 50 op-stemmer på en dag.\n      higher_love:\n        name:\n          other: Højere Kærlighed\n        desc:\n          other: Brugte 50 stemmer på en dag 5 gange.\n      crazy_in_love:\n        name:\n          other: Vanvittig forelsket\n        desc:\n          other: Brugte 50 op-stemmer på en dag 20 gange.\n      promoter:\n        name:\n          other: Promovør\n        desc:\n          other: Inviterede en bruger.\n      campaigner:\n        name:\n          other: Kampagnefører\n        desc:\n          other: Inviterede 3 basis-brugere.\n      champion:\n        name:\n          other: Mester\n        desc:\n          other: Inviterede 5 medlemmer.\n      thank_you:\n        name:\n          other: Tak\n        desc:\n          other: Har 20 op-stemte opslag og afgav 10 op-stemmer.\n      gives_back:\n        name:\n          other: Giver Tilbage\n        desc:\n          other: Har 100 op-stemte opslag og afgav 100 op-stemmer.\n      empathetic:\n        name:\n          other: Empatisk\n        desc:\n          other: Har 500 op-stemte opslag og afgav 1000 op-stemmer.\n      enthusiast:\n        name:\n          other: Entusiast\n        desc:\n          other: Besøgte 10 på hinanden følgende dage.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Besøgte 100 på hinanden følgende dage.\n      devotee:\n        name:\n          other: Hengiven\n        desc:\n          other: Besøgte 365 på hinanden følgende dage.\n      anniversary:\n        name:\n          other: Jubilæum\n        desc:\n          other: Aktivt medlem i et år, oprettet indlæg mindst én gang.\n      appreciated:\n        name:\n          other: Værdsat\n        desc:\n          other: Modtog 1 op-stemme på 20 opslag.\n      respected:\n        name:\n          other: Respekteret\n        desc:\n          other: Modtog 2 op-stemmer på 100 opslag.\n      admired:\n        name:\n          other: Beundret\n        desc:\n          other: Modtog 5 op-stemmer på 300 indlæg.\n      solved:\n        name:\n          other: Løst\n        desc:\n          other: Har fået et svar accepteret.\n      guidance_counsellor:\n        name:\n          other: Vejledningsrådgiver\n        desc:\n          other: Har fået 10 svar accepteret.\n      know_it_all:\n        name:\n          other: Bedrevidende\n        desc:\n          other: Har fået 50 svar accepteret.\n      solution_institution:\n        name:\n          other: Institution For Løsning\n        desc:\n          other: Har fået 150 svar accepteret.\n      nice_answer:\n        name:\n          other: Dejligt Svar\n        desc:\n          other: Besvar score på 10 eller mere.\n      good_answer:\n        name:\n          other: Godt Svar\n        desc:\n          other: Besvar score på 25 eller mere.\n      great_answer:\n        name:\n          other: Fantastisk Svar\n        desc:\n          other: Besvar score på 50 eller mere.\n      nice_question:\n        name:\n          other: Godt Spørgsmål\n        desc:\n          other: Spørgsmål score på 10 eller mere.\n      good_question:\n        name:\n          other: Godt Spørgsmål\n        desc:\n          other: Spørgsmål score på 25 eller mere.\n      great_question:\n        name:\n          other: Fantastisk Spørgsmål\n        desc:\n          other: Spørgsmål score på 50 eller mere.\n      popular_question:\n        name:\n          other: Populært Spørgsmål\n        desc:\n          other: Spørgsmål med 500 visninger.\n      notable_question:\n        name:\n          other: Bemærkelsesværdigt Spørgsmål\n        desc:\n          other: Spørgsmål med 1.000 visninger.\n      famous_question:\n        name:\n          other: Berømt Spørgsmål\n        desc:\n          other: Spørgsmål med 5.000 visninger.\n      popular_link:\n        name:\n          other: Populært Link\n        desc:\n          other: Sendt et eksternt link med 50 kliks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Sendt et eksternt link med 300 kliks.\n      famous_link:\n        name:\n          other: Berømt Link\n        desc:\n          other: Sendt et eksternt link med 100 kliks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Sådan kommer du igang\n      community:\n        name:\n          other: Fællesskab\n      posting:\n        name:\n          other: Oplæg\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Sådan formaterer du\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">for at lave links</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Titel](https://url.com)</code></pre></li><li><p class=\"mb-2\">indsæt linieskift mellem paragraffer</p></li><li><p class=\"mb-2\"><em>_kursiv_</em> or **<strong>fed</strong>**</p></li><li><p class=\"mb-2\">indskyd kode med 4 mellemrum</p></li><li><p class=\"mb-2\">citér ved at placere <code>&gt;</code> på starten af linie</p></li><li><p class=\"mb-2\">backtick escapes <code>`som _dette_`</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>\n  pagination:\n    prev: Forrige\n    next: Næste\n  page_title:\n    question: Spørgsmål\n    questions: Spørgsmål\n    tag: Nøgleord\n    tags: Nøgleord\n    tag_wiki: nøgleord info\n    create_tag: Opret nøgleord\n    edit_tag: Rediger nøgleord\n    ask_a_question: Opret spørgsmål\n    edit_question: Rediger spørgsmål\n    edit_answer: Rediger Svar\n    search: Søg\n    posts_containing: Opslag som indeholder\n    settings: Indstillinger\n    notifications: Notifikationer\n    login: Log Ind\n    sign_up: Tilmeld dig\n    account_recovery: Konto-gendannelse\n    account_activation: Aktivering af konto\n    confirm_email: Bekræft e-mail\n    account_suspended: Konto suspenderet\n    admin: Administrator\n    change_email: Ændre E-Mail\n    install: Answer Installation\n    upgrade: Answer Opgradering\n    maintenance: Vedligeholdelse af websted\n    users: Brugere\n    oauth_callback: Behandler\n    http_404: HTTP Fejl 404\n    http_50X: Http Fejl 500\n    http_403: HTTP Fejl 403\n    logout: Log Ud\n    posts: Opslag\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifikationer\n    inbox: Indbakke\n    achievement: Bedrifter\n    new_alerts: Nye adviseringer\n    all_read: Markér alle som læst\n    show_more: Vis mere\n    someone: Nogen\n    inbox_type:\n      all: Alle\n      posts: Opslag\n      invites: Invitationer\n      votes: Stemmer\n    answer: Svar\n    question: Spørgsmål\n    badge_award: emblem\n  suspended:\n    title: Din konto er blevet suspenderet\n    until_time: \"Din konto blev suspenderet indtil {{ time }}.\"\n    forever: Denne bruger blev suspenderet for evigt.\n    end: Du opfylder ikke en fællesskabsretningslinje.\n    contact_us: Kontakt os\n  editor:\n    blockquote:\n      text: Citatblok\n    bold:\n      text: Fed\n    chart:\n      text: Diagram\n      flow_chart: Flow- diagram\n      sequence_diagram: Sekvensdiagram\n      class_diagram: Klassediagram\n      state_diagram: Tilstands-diagram\n      entity_relationship_diagram: Enheds-forhold-diagram\n      user_defined_diagram: Brugerdefineret diagram\n      gantt_chart: Gantt- diagram\n      pie_chart: Cirkeldiagram\n    code:\n      text: Kode-eksempel\n      add_code: Tilføj kodeeksempel\n      form:\n        fields:\n          code:\n            label: Kode\n            msg:\n              empty: Kode skal udfyldes.\n          language:\n            label: Sprog\n            placeholder: Automatisk detektering\n      btn_cancel: Annuller\n      btn_confirm: Tilføj\n    formula:\n      text: Formel\n      options:\n        inline: Indlejret formel\n        block: Formel blok\n    heading:\n      text: Overskrift\n      options:\n        h1: Overskrift 1\n        h2: Overskrift 2\n        h3: Overskrift 3\n        h4: Overskrift 4\n        h5: Overskrift 5\n        h6: Overskrift 6\n    help:\n      text: Hjælp\n    hr:\n      text: Vandret streg\n    image:\n      text: Billede\n      add_image: Tilføj billede\n      tab_image: Upload billede\n      form_image:\n        fields:\n          file:\n            label: Billedfil\n            btn: Vælg billede\n            msg:\n              empty: Filen skal udfyldes.\n              only_image: Kun billedfiler er tilladt.\n              max_size: Filstørrelse må ikke overstige {{size}} MB.\n          desc:\n            label: Beskriveslse\n      tab_url: Billede-URL\n      form_url:\n        fields:\n          url:\n            label: Billede-URL\n            msg:\n              empty: Billede-URL skal udfyldes.\n          name:\n            label: Beskriveslse\n      btn_cancel: Annuller\n      btn_confirm: Tilføj\n      uploading: Uploader\n    indent:\n      text: Indrykning\n    outdent:\n      text: Udrykning\n    italic:\n      text: Fremhævning\n    link:\n      text: Link\n      add_link: Tilføj link\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL må ikke være tom.\n          name:\n            label: Beskriveslse\n      btn_cancel: Annuller\n      btn_confirm: Tilføj\n    ordered_list:\n      text: Nummereret liste\n    unordered_list:\n      text: Punktliste\n    table:\n      text: Tabel\n      heading: Overskrift\n      cell: Celle\n    file:\n      text: Vedhæft filer\n      not_supported: \"Understøtter ikke denne filtype. Prøv igen med {{file_type}}.\"\n      max_size: \"Vedhæftet filers størrelse kan ikke overstige {{size}} MB.\"\n  close_modal:\n    title: Jeg lukker dette indlæg fordi...\n    btn_cancel: Annuller\n    btn_submit: Indsend\n    remark:\n      empty: skal udfyldes.\n    msg:\n      empty: Vælg en grund.\n  report_modal:\n    flag_title: Jeg markerer for at rapportere dette indlæg som...\n    close_title: Jeg lukker dette indlæg fordi...\n    review_question_title: Gennemgå spørgsmål\n    review_answer_title: Gennemgå svar\n    review_comment_title: Gennemgå kommentar\n    btn_cancel: Annuller\n    btn_submit: Indsend\n    remark:\n      empty: skal udfyldes.\n    msg:\n      empty: Vælg en grund.\n      not_a_url: URL-format er forkert.\n      url_not_match: URL oprindelsen matcher ikke det aktuelle websted.\n  tag_modal:\n    title: Opret et nyt nøgleord\n    form:\n      fields:\n        display_name:\n          label: Visnings-navn\n          msg:\n            empty: Visnings-navn skal udfyldes.\n            range: Visnings-navn på op til 35 tegn.\n        slug_name:\n          label: URL-slug\n          desc: URL slug op til 35 tegn.\n          msg:\n            empty: URL slug må ikke være tom.\n            range: URL slug op til 35 tegn.\n            character: URL slug indeholder ikke tilladte tegn.\n        desc:\n          label: Beskriveslse\n        revision:\n          label: Revision\n        edit_summary:\n          label: Rediger resumé\n          placeholder: >-\n            Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering)\n    btn_cancel: Annuller\n    btn_submit: Indsend\n    btn_post: Send nyt tag\n  tag_info:\n    created_at: Oprettet\n    edited_at: Redigeret\n    history: Historik\n    synonyms:\n      title: Synonymer\n      text: Følgende tags vil blive genmappet til\n      empty: Ingen synonymer fundet.\n      btn_add: Tilføj et synonym\n      btn_edit: Rediger\n      btn_save: Gem\n    synonyms_text: Følgende nøgleord vil blive genmappet til\n    delete:\n      title: Slet dette nøgleord\n      tip_with_posts: >-\n        <p>Vi tillader ikke <strong>at slette nøgleord med opslag</strong>.</p> <p>Fjern venligst dette nøgleord fra opslagene først.</p>\n      tip_with_synonyms: >-\n        <p>Vi tillader ikke <strong>at slette tag med indlæg</strong>.</p> <p>Fjern venligst dette tag fra indlæggene først.</p>\n      tip: Er du sikker på, at du vil slette?\n      close: Luk\n    merge:\n      title: Sammenflet nøgleord\n      source_tag_title: Kilde nøgleord\n      source_tag_description: Kildenøgleordet og dets tilknyttede data vil blive omlagt til målmærket.\n      target_tag_title: Målnøgleord\n      target_tag_description: Et synonym mellem disse to nøgleord vil blive oprettet efter sammenlægning.\n      no_results: Ingen nøgleord matchede\n      btn_submit: Indsend\n      btn_close: Luk\n  edit_tag:\n    title: Rediger nøgleord\n    default_reason: Rediger nøgleord\n    default_first_reason: Tilføj nøgleord\n    btn_save_edits: Gem ændringer\n    btn_cancel: Annuller\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"D MMMM, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [kl.] HH:mm\"\n    now: nu\n    x_seconds_ago: \"{{count}}s siden\"\n    x_minutes_ago: \"{{count}}s siden\"\n    x_hours_ago: \"{{count}}t siden\"\n    hour: time\n    day: dag\n    hours: timer\n    days: dag\n    month: måned\n    months: måneder\n    year: År\n  reaction:\n    heart: hjerte\n    smile: smil\n    frown: rynke panden\n    btn_label: tilføj eller fjern reaktioner\n    undo_emoji: fortryd {{ emoji }} reaktion\n    react_emoji: reager med {{ emoji }}\n    unreact_emoji: ikke reager med {{ emoji }}\n  comment:\n    btn_add_comment: Tilføj kommentar\n    reply_to: Svar til\n    btn_reply: Svar\n    btn_edit: Rediger\n    btn_delete: Slet\n    btn_flag: Anmeld\n    btn_save_edits: Gem ændringer\n    btn_cancel: Annuller\n    show_more: \"{{count}} flere kommentarer\"\n    tip_question: >-\n      Brug kommentarer til at bede om mere information eller foreslå forbedringer. Undgå at besvare spørgsmål i kommentarer.\n    tip_answer: >-\n      Brug kommentarer til at svare andre brugere eller give dem besked om ændringer. Hvis du tilføjer nye oplysninger, skal du redigere dit indlæg i stedet for at kommentere.\n    tip_vote: Det tilføjer noget nyttigt til indlægget\n  edit_answer:\n    title: Rediger Svar\n    default_reason: Rediger svar\n    default_first_reason: Tilføj svar\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Svar\n          feedback:\n            characters: indhold skal være mindst 6 tegn.\n        edit_summary:\n          label: Rediger resumé\n          placeholder: >-\n            Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering)\n    btn_save_edits: Gem ændringer\n    btn_cancel: Annuller\n  tags:\n    title: Nøgleord\n    sort_buttons:\n      popular: Populære\n      name: Navn\n      newest: Nyeste\n    button_follow: Følg\n    button_following: Følger\n    tag_label: spørgsmål\n    search_placeholder: Filtrer efter nøgleord-navn\n    no_desc: Nøgleord har ingen beskrivelse.\n    more: Mere\n    wiki: Info\n  ask:\n    title: Opret spørgsmål\n    edit_title: Rediger spørgsmål\n    default_reason: Rediger spørgsmål\n    default_first_reason: Opret spørgsmål\n    similar_questions: Lignende spørgsmål\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Titel\n          placeholder: Hvad er dit emne? Vær specifik.\n          msg:\n            empty: Titel må ikke være tom.\n            range: Titel på op til 150 tegn\n        body:\n          label: Brødtekst\n          msg:\n            empty: Brødtekst skal udfyldes.\n          hint:\n            optional_body: Beskriv hvad spørgsmålet handler om.\n            minimum_characters: \"Beskriv hvad spørgsmålet handler om, mindst {{min_content_length}} tegn er påkrævet.\"\n        tags:\n          label: Nøgleord\n          msg:\n            empty: Nøgleord må ikke være tom.\n        answer:\n          label: Svar\n          msg:\n            empty: Svar må ikke være tomt.\n        edit_summary:\n          label: Rediger resumé\n          placeholder: >-\n            Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering)\n    btn_post_question: Indsend dit spørgsmål\n    btn_save_edits: Gem ændringer\n    answer_question: Besvar dit eget spørgsmål\n    post_question&answer: Send dit spørgsmål og svar\n  tag_selector:\n    add_btn: Tilføj nøgleord\n    create_btn: Opret et nyt nøgleord\n    search_tag: Søg nøgleord\n    hint: Beskriv hvad dit spørgsmål handler om, mindst et nøgleord er påkrævet.\n    hint_zero_tags: Beskriv hvad dit indhold handler om.\n    hint_more_than_one_tag: \"Beskriv hvad dit indhold handler om, i det mindste {{min_tags_number}} nøgleord er påkrævet.\"\n    no_result: Ingen nøgleord matchede\n    tag_required_text: Påkrævet nøgleord (mindst én)\n  header:\n    nav:\n      question: Spørgsmål\n      tag: Nøgleord\n      user: Brugere\n      badges: Emblemer\n      profile: Profil\n      setting: Indstillinger\n      logout: Log Ud\n      admin: Administrator\n      review: Gennemgå\n      bookmark: Bogmærker\n      moderation: Moderering\n    search:\n      placeholder: Søg\n  footer:\n    build_on: Drevet af <1>Apache Answer</1>\n  upload_img:\n    name: Skift\n    loading: indlæser...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Skriv teksten ovenfor\n    msg:\n      empty: Captcha må ikke være tomt.\n  inactive:\n    first: >-\n      Du er næsten færdig! Vi har sendt en aktiveringsmail til <bold>{{mail}}</bold>. Følg venligst instruktionerne i mailen for at aktivere din konto.\n    info: \"Hvis det ikke ankommer, tjek din spam-mappe.\"\n    another: >-\n      Vi har sendt endnu en aktiverings-e-mail til dig på <bold>{{mail}}</bold>. Det kan tage nogen få minutter før den når frem; kontrollér også din spam-mappe.\n    btn_name: Send aktiverings-e-mail igen\n    change_btn_name: Ændre e-mail\n    msg:\n      empty: skal udfyldes.\n    resend_email:\n      url_label: Er du sikker på, at du vil sende aktiveringse-mailen?\n      url_text: Du kan også give aktiveringslinket ovenfor til brugeren.\n  login:\n    login_to_continue: Log ind for at fortsætte\n    info_sign: Har du ikke en konto? <1>Tilmeld dig </1>\n    info_login: Har du allerede en konto? <1>Log ind </1>\n    agreements: Ved at registrere dig accepterer du <1>privacy policy</1> og <3>terms of service </3>.\n    forgot_pass: Glemt adgangskoden?\n    name:\n      label: Navn\n      msg:\n        empty: Navn må ikke være tomt.\n        range: Navn skal være mellem 2 og 30 tegn i længden.\n        character: 'Skal bruge tegnsættet \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail skal udfyldes.\n    password:\n      label: Adgangskode\n      msg:\n        empty: Adgangskoden skal udfyldes.\n        different: De indtastede adgangskoder er ikke ens\n  account_forgot:\n    page_title: Glemt adgangskode\n    btn_name: Send mig gendannelsesmail\n    send_success: >-\n      Hvis en konto matcher <strong>{{mail}}</strong>, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode.\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail skal udfyldes.\n  change_email:\n    btn_cancel: Annuller\n    btn_update: Opdater e-mailadresse\n    send_success: >-\n      Hvis en konto matcher <strong>{{mail}}</strong>, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode.\n    email:\n      label: Ny e-mail\n      msg:\n        empty: E-mail skal udfyldes.\n  oauth:\n    connect: Forbind med {{ auth_name }}\n    remove: Fjern {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Tilføj en gendannelsese-mail til din konto.\n    btn_update: Opdater e-mailadresse\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail skal udfyldes.\n    modal_title: Email eksisterer allerede.\n    modal_content: Denne e-mailadresse er allerede registreret. Er du sikker på, at du vil oprette forbindelse til den eksisterende konto?\n    modal_cancel: Ændre e-mail\n    modal_confirm: Opret forbindelse til den eksisterende konto\n  password_reset:\n    page_title: Nulstil adgangskode\n    btn_name: Nulstil min adgangskode\n    reset_success: >-\n      Du har ændret din adgangskode. Du vil blive omdirigeret til siden log ind.\n    link_invalid: >-\n      Beklager, dette link til nulstilling af adgangskode er ikke længere gyldigt. Måske er din adgangskode allerede nulstillet?\n    to_login: Fortsæt til log-ind siden\n    password:\n      label: Adgangskode\n      msg:\n        empty: Adgangskoden skal udfyldes.\n        length: Længden skal være mellem 8 og 32 tegn\n        different: De indtastede adgangskoder er ikke ens\n    password_confirm:\n      label: Bekræft den nye adgangskode\n  settings:\n    page_title: Indstillinger\n    goto_modify: Gå til at ændre\n    nav:\n      profile: Profil\n      notification: Notifikationer\n      account: Konto\n      interface: Grænseflade\n    profile:\n      heading: Profil\n      btn_name: Gem\n      display_name:\n        label: Visnings-navn\n        msg: Visnings-navn skal udfyldes.\n        msg_range: Visningsnavnet skal være 2-30 tegn i længden.\n      username:\n        label: Brugernavn\n        caption: Man kan nævne dig som \"@username\".\n        msg: Brugernavn skal udfyldes.\n        msg_range: Brugernavn skal være 2-30 tegn i længden.\n        character: 'Skal bruge tegnsættet \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profilbillede\n        gravatar: Gravatar\n        gravatar_text: Du kan ændre billede på\n        custom: Brugerdefineret\n        custom_text: Du kan uploade dit billede.\n        default: System\n        msg: Upload en avatar\n      bio:\n        label: Om mig\n      website:\n        label: Websted\n        placeholder: \"https://example.com\"\n        msg: Forkert format på websted\n      location:\n        label: Placering\n        placeholder: \"By, land\"\n    notification:\n      heading: Email-notifikationer\n      turn_on: Slå til\n      inbox:\n        label: Notifikationer i indbakken\n        description: Svar på dine spørgsmål, kommentarer, invitationer og mere.\n      all_new_question:\n        label: Alle nye spørgsmål\n        description: Få besked om alle nye spørgsmål. Op til 50 spørgsmål om ugen.\n      all_new_question_for_following_tags:\n        label: Alle nye spørgsmål til følgende nøgleord\n        description: Få besked om nye spørgsmål til følgende nøgleord.\n    account:\n      heading: Konto\n      change_email_btn: Ændre e-mail\n      change_pass_btn: Skift adgangskode\n      change_email_info: >-\n        Vi har sendt en e-mail til denne adresse. Følg venligst bekræftelsesinstruktionerne.\n      email:\n        label: E-mail\n      new_email:\n        label: Ny e-mail\n        msg: Ny e-mail skal udfyldes.\n      pass:\n        label: Nuværende adgangskode\n        msg: Adgangskoden skal udfyldes.\n      password_title: Adgangskode\n      current_pass:\n        label: Nuværende adgangskode\n        msg:\n          empty: Nuværende adgangskode skal udfyldes.\n          length: Længden skal være mellem 8 og 32 tegn.\n          different: De to indtastede adgangskoder er ikke ens.\n      new_pass:\n        label: Ny adgangskode\n      pass_confirm:\n        label: Bekræft den nye adgangskode\n    interface:\n      heading: Grænseflade\n      lang:\n        label: Grænseflade sprog\n        text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden.\n    my_logins:\n      title: Mine log ind\n      label: Log ind eller tilmeld dig på dette websted ved hjælp af disse konti.\n      modal_title: Fjern login\n      modal_content: Er du sikker på, at du vil fjerne dette login fra din konto?\n      modal_confirm_btn: Slet\n      remove_success: Fjernet\n  toast:\n    update: opdatering gennemført\n    update_password: Adgangskoden er ændret.\n    flag_success: Tak for at anmelde.\n    forbidden_operate_self: Forbudt at operere på dig selv\n    review: Din revision vil blive vist efter gennemgang.\n    sent_success: Sendt\n  related_question:\n    title: Relateret\n    answers: svar\n  linked_question:\n    title: Knyttet\n    description: Opslag knyttet til\n    no_linked_question: Intet indhold linket fra dette indhold.\n  invite_to_answer:\n    title: Inviter personer\n    desc: Inviter personer, som du tror, kan svare.\n    invite: Inviter til at svare\n    add: Tilføj personer\n    search: Søg personer\n  question_detail:\n    action: Handling\n    created: Oprettet\n    Asked: Spurgt\n    asked: spurgt\n    update: Ændret\n    Edited: Redigeret\n    edit: redigeret\n    commented: kommenteret\n    Views: Set\n    Follow: Følg\n    Following: Følger\n    follow_tip: Følg dette spørgsmål for at modtage notifikationer\n    answered: besvaret\n    closed_in: Lukket om\n    show_exist: Vis eksisterende spørgsmål.\n    useful: Nyttigt\n    question_useful: Det er nyttigt og klart\n    question_un_useful: Det er uklart eller ikke nyttigt\n    question_bookmark: Bogmærk dette spørgsmål\n    answer_useful: Det er nyttigt\n    answer_un_useful: Det er ikke nyttigt\n    answers:\n      title: Svar\n      score: Bedømmelse\n      newest: Nyeste\n      oldest: Ældste\n      btn_accept: Acceptér\n      btn_accepted: Accepteret\n    write_answer:\n      title: Dit Svar\n      edit_answer: Redigér mit eksisterende svar\n      btn_name: Indsend dit svar\n      add_another_answer: Tilføj endnu et svar\n      confirm_title: Fortsæt med at svare\n      continue: Forsæt\n      confirm_info: >-\n        <p>Er du sikker på, at du vil tilføje et andet svar?</p><p>Du kan i stedet bruge redigeringslinket til at forfine og forbedre dit eksisterende svar.</p>\n      empty: Svar skal udfyldes.\n      characters: indhold skal være mindst 6 tegn.\n      tips:\n        header_1: Tak for dit svar\n        li1_1: Vær sikker på at <strong>besvare spørgsmålet</strong>. Giv oplysninger og del din forskning.\n        li1_2: Begrund eventuelle udsagn med referencer eller personlige erfaringer.\n        header_2: Men <strong>undgå</strong>...\n        li2_1: Spørger om hjælp, søger afklaring, eller reagerer på andre svar.\n    reopen:\n      confirm_btn: Genåbn\n      title: Genåbn dette indlæg\n      content: Er du sikker på, at du vil genåbne?\n    list:\n      confirm_btn: Liste\n      title: Sæt dette indlæg på listen\n      content: Er du sikker på du vil sætte på listen?\n    unlist:\n      confirm_btn: Fjern fra listen\n      title: Fjern dette indlæg fra listen\n      content: Er du sikker på at du vil fjerne fra listen?\n    pin:\n      title: Fastgør dette indlæg\n      content: Er du sikker på, at du ønsker at fastgøre globalt? Dette indlæg vises øverst på alle indlægs-lister.\n      confirm_btn: Fastgør\n  delete:\n    title: Slet dette indlæg\n    question: >-\n      Vi anbefaler ikke, at <strong>sletter spørgsmål med svar</strong>, fordi det fratager fremtidige læsere denne viden.</p><p>Gentaget sletning af besvarede spørgsmål kan resultere i, at din konto bliver blokeret fra at spørge. Er du sikker på, at du ønsker at slette?\n    answer_accepted: >-\n      <p>Vi anbefaler ikke <strong>at slette accepteret svar</strong> fordi det fratager fremtidige læsere denne viden. </p> Gentagen sletning af accepterede svar kan resultere i, at din konto bliver blokeret fra besvarelse. Er du sikker på, at du ønsker at slette?\n    other: Er du sikker på, at du vil slette?\n    tip_answer_deleted: Dette svar er blevet slettet\n    undelete_title: Genopret dette indlæg\n    undelete_desc: Er du sikker på du ønsker at genoprette?\n  btns:\n    confirm: Bekræft\n    cancel: Annuller\n    edit: Rediger\n    save: Gem\n    delete: Slet\n    undelete: Genopret\n    list: Sæt på liste\n    unlist: Fjern fra liste\n    unlisted: Fjernet fra liste\n    login: Log ind\n    signup: Opret konto\n    logout: Log Ud\n    verify: Verificér\n    create: Opret\n    approve: Godkend\n    reject: Afvis\n    skip: Spring Over\n    discard_draft: Kassér udkast\n    pinned: Fastgjort\n    all: Alle\n    question: Spørgsmål\n    answer: Svar\n    comment: Kommentar\n    refresh: Genopfrisk\n    resend: Send igen\n    deactivate: Deaktiver\n    active: Aktiv\n    suspend: Suspendér\n    unsuspend: Ophæv suspendering\n    close: Luk\n    reopen: Genåbn\n    ok: Ok\n    light: Lys\n    dark: Mørk\n    system_setting: Systemindstilling\n    default: Standard\n    reset: Nulstil\n    tag: Nøgleord\n    post_lowercase: indlæg\n    filter: Filtrer\n    ignore: Ignorér\n    submit: Indsend\n    normal: Normal\n    closed: Lukket\n    deleted: Slettet\n    deleted_permanently: Slettet permanent\n    pending: Ventende\n    more: Mere\n    view: Vis\n    card: Kort\n    compact: Kompakt\n    display_below: Vis nedenfor\n    always_display: Vis altid\n    or: eller\n    back_sites: Tilbage til websteder\n  search:\n    title: Søgeresultater\n    keywords: Nøgleord\n    options: Muligheder\n    follow: Følg\n    following: Følger\n    counts: \"{{count}} Resultater\"\n    counts_loading: \"... Resultater\"\n    more: Mere\n    sort_btns:\n      relevance: Relevans\n      newest: Nyeste\n      active: Aktiv\n      score: Bedømmelse\n      more: Mere\n    tips:\n      title: Avancerede Søgetips\n      tag: \"<1>[tag]</1> søgning med et nøgleord\"\n      user: \"<1>user:username</1> søgning efter forfatter\"\n      answer: \"<1>answers:0</1> ubesvarede spørgsmål\"\n      score: \"<1>score:3</1> opslag med 3+ score\"\n      question: \"<1>is:question</1> søgespørgsmål\"\n      is_answer: \"<1>is:answer</1> søgesvar\"\n    empty: Vi kunne ikke finde noget. <br /> Prøv forskellige eller mindre specifikke søgeord.\n  share:\n    name: Del\n    copy: Kopiér link\n    via: Del indlæg via...\n    copied: Kopieret\n    facebook: Del på Facebook\n    twitter: Del til X\n  cannot_vote_for_self: Du kan ikke stemme på dit eget indlæg.\n  modal_confirm:\n    title: Fejl...\n  delete_permanently:\n    title: Slet permanent\n    content: Er du sikker på du vil slette permanent?\n  account_result:\n    success: Din nye konto er bekræftet. Du vil blive omdirigeret til hjemmesiden.\n    link: Fortsæt til startside\n    oops: Hovsa!\n    invalid: Linket, du brugte, virker ikke længere.\n    confirm_new_email: Din e-mail er blevet opdateret.\n    confirm_new_email_invalid: >-\n      Beklager, dette bekræftelseslink er ikke længere gyldigt. Måske blev din e-mail allerede ændret?\n  unsubscribe:\n    page_title: Afmeld\n    success_title: Afmelding Lykkedes\n    success_desc: Du er blevet fjernet fra denne abonnentliste og vil ikke modtage yderligere e-mails fra os.\n    link: Skift indstillinger\n  question:\n    following_tags: Følger Nøgleord\n    edit: Rediger\n    save: Gem\n    follow_tag_tip: Følg nøgleord for at udvælge dine spørgsmål.\n    hot_questions: Populære Spørgsmål\n    all_questions: Alle Spørgsmål\n    x_questions: \"{{ count }} Spørgsmål\"\n    x_answers: \"{{ count }} svar\"\n    x_posts: \"{{ count }} Opslag\"\n    questions: Spørgsmål\n    answers: Svar\n    newest: Nyeste\n    active: Aktiv\n    hot: Populært\n    frequent: Ofte\n    recommend: Anbefal\n    score: Bedømmelse\n    unanswered: Ubesvaret\n    modified: ændret\n    answered: besvaret\n    asked: spurgt\n    closed: lukket\n    follow_a_tag: \"Følg et nøgleord\\n\"\n    more: Mere\n  personal:\n    overview: Oversigt\n    answers: Svar\n    answer: svar\n    questions: Spørgsmål\n    question: spørgsmål\n    bookmarks: Bogmærker\n    reputation: Omdømme\n    comments: Kommentarer\n    votes: Stemmer\n    badges: Emblemer\n    newest: Nyeste\n    score: Bedømmelse\n    edit_profile: Rediger profil\n    visited_x_days: \"Besøgte {{ count }} dage\"\n    viewed: Set\n    joined: Tilmeldt\n    comma: \",\"\n    last_login: Set\n    about_me: Om Mig\n    about_me_empty: \"// Hej, Verden!\"\n    top_answers: Populære Svar\n    top_questions: Populære Spørgsmål\n    stats: Statistik\n    list_empty: Ingen opslag fundet.<br />Måske vil du vælge en anden fane?\n    content_empty: Ingen opslag fundet.\n    accepted: Accepteret\n    answered: besvaret\n    asked: spurgt\n    downvoted: nedstemt\n    mod_short: MOD\n    mod_long: Moderatorer\n    x_reputation: omdømme\n    x_votes: stemmer modtaget\n    x_answers: svar\n    x_questions: spørgsmål\n    recent_badges: Seneste emblemer\n  install:\n    title: Installation\n    next: Næste\n    done: Udført\n    config_yaml_error: Kan ikke oprette filen config.yaml.\n    lang:\n      label: Vælg et sprog\n    db_type:\n      label: Database type\n    db_username:\n      label: Brugernavn\n      placeholder: rod\n      msg: Brugernavn skal udfyldes.\n    db_password:\n      label: Adgangskode\n      placeholder: rod\n      msg: Adgangskoden skal udfyldes.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host skal udfyldes.\n    db_name:\n      label: Database navn\n      placeholder: answer\n      msg: Databasenavn skal udfyldes.\n    db_file:\n      label: Databasefil\n      placeholder: /data/answer.db\n      msg: Databasefil skal udfyldes.\n    ssl_enabled:\n      label: Aktiver SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL-tilstand\n    ssl_root_cert:\n      placeholder: sslrootcert filsti\n      msg: Sti til sslrootcert fil kan ikke være tom\n    ssl_cert:\n      placeholder: sslrootcert filsti\n      msg: Sti til sslrootcert fil kan ikke være tom\n    ssl_key:\n      placeholder: sslrootcert filsti\n      msg: Sti til sslrootcert fil kan ikke være tom\n    config_yaml:\n      title: Opret config.yaml\n      label: Filen config.yaml blev oprettet.\n      desc: >-\n        Du kan manuelt oprette filen <1>config.yaml</1> i mappen <1>/var/wwww/xxx/</1> og indsætte følgende tekst i den.\n      info: Når du har gjort det, skal du klikke på \"Næste\" knappen.\n    site_information: Websted Information\n    admin_account: Administrator Konto\n    site_name:\n      label: Websted navn\n      msg: Websted-navn skal udfyldes.\n      msg_max_length: Webstedsnavn kan ikke være længere end 30 tegn.\n    site_url:\n      label: Websted URL\n      text: Adressen på dit websted.\n      msg:\n        empty: Webstedets URL skal udfyldes.\n        incorrect: Websteds URL forkert format.\n        max_length: WebstedsURL skal højst være 512 tegn.\n    contact_email:\n      label: Kontakt e-mail\n      text: E-mailadresse på nøglekontakt ansvarlig for dette websted.\n      msg:\n        empty: Kontakt-e-mail skal udfyldes.\n        incorrect: Ugyldig kontakt e-mail adresse.\n    login_required:\n      label: Privat\n      switch: Log ind påkrævet\n      text: Kun brugere som er logget ind har adgang til dette fællesskab.\n    admin_name:\n      label: Navn\n      msg: Navn skal udfyldes.\n      character: 'Skal bruge tegnsættet \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Navn skal være mellem 2 og 30 tegn i længden.\n    admin_password:\n      label: Adgangskode\n      text: >-\n        Du skal bruge denne adgangskode for at logge ind. Opbevar den et sikkert sted.\n      msg: Adgangskoden skal udfyldes.\n      msg_min_length: Adgangskoden skal være mindst 8 tegn.\n      msg_max_length: Adgangskoden skal højst udgøre 32 tegn.\n    admin_confirm_password:\n      label: \"Bekræft adgangskode\"\n      text: \"Indtast venligst din adgangskode igen for at bekræfte.\"\n      msg: \"Bekræft adgangskoden stemmer ikke overens.\"\n    admin_email:\n      label: E-mail\n      text: Du skal bruge denne e-mail for at logge ind.\n      msg:\n        empty: E-mail skal udfyldes.\n        incorrect: Ugyldig e-mail adresse.\n    ready_title: Dit websted er klar\n    ready_desc: >-\n      Hvis du nogensinde har lyst til at ændre flere indstillinger, kan du besøge <1>admin-sektion</1>; find det i site-menuen.\n    good_luck: \"Hav det sjovt, og held og lykke!\"\n    warn_title: Advarsel\n    warn_desc: >-\n      Filen <1>config.yaml</1> findes allerede. Hvis du har brug for at nulstille en af konfigurationselementerne i denne fil, så slet den først.\n    install_now: Du kan prøve <1>at installere nu</1>.\n    installed: Allerede installeret\n    installed_desc: >-\n      Du synes allerede at være installeret. For at geninstallere skal du først rydde dine gamle databasetabeller.\n    db_failed: Database forbindelse mislykkedes\n    db_failed_desc: >-\n      Det betyder enten, at databaseinformationen i din <1>config. aml</1> fil er forkert eller at kontakt med databaseserveren ikke kunne etableres. Dette kan betyde, at din værts databaseserver er nede.\n  counts:\n    views: visninger\n    votes: stemmer\n    answers: svar\n    accepted: Accepteret\n  page_error:\n    http_error: HTTP Fejl {{ code }}\n    desc_403: Du har ikke adgang til denne side.\n    desc_404: Denne side findes desværre ikke.\n    desc_50X: Der skete en fejl på serveren og den kunne ikke fuldføre din anmodning.\n    back_home: Tilbage til forsiden\n  page_maintenance:\n    desc: \"Vi laver vedligeholdelse, men er snart tilbage igen.\"\n  nav_menus:\n    dashboard: Kontrolpanel\n    contents: Indhold\n    questions: Spørgsmål\n    answers: Svar\n    users: Brugere\n    badges: Emblemer\n    flags: Anmeldelser\n    settings: Indstillinger\n    general: Generelt\n    interface: Brugerflade\n    smtp: SMTP\n    branding: Branding\n    legal: Jura\n    write: Skrivning\n    terms: Vilkår\n    tos: Betingelser for brug\n    privacy: Privatliv\n    seo: SEO\n    customize: Tilpas\n    themes: Temaer\n    login: Log Ind\n    privileges: Rettigheder\n    plugins: Plugins\n    installed_plugins: Installerede Plugins\n    apperance: Udseende\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Velkommen til {{site_name}}\n  user_center:\n    login: Log Ind\n    qrcode_login_tip: Brug {{ agentName }} til at scanne QR-koden og logge ind.\n    login_failed_email_tip: Log ind mislykkedes, tillad denne app at få adgang til dine e-mail-oplysninger, før du prøver igen.\n  badges:\n    modal:\n      title: Tillykke\n      content: Du har optjent et nyt badge.\n      close: Luk\n      confirm: Se emblemer\n    title: emblem\n    awarded: Tildelt\n    earned_×: Optjent ×{{ number }}\n    ×_awarded: \"{{ number }} tildelt\"\n    can_earn_multiple: Du kan tjene dette flere gange.\n    earned: Optjent\n  admin:\n    admin_header:\n      title: Administrator\n    dashboard:\n      title: Kontrolpanel\n      welcome: Velkommen til Administration!\n      site_statistics: Statistik for webstedet\n      questions: \"Spørgsmål:\"\n      resolved: \"Løst\"\n      unanswered: \"Ubesvaret:\"\n      answers: \"Svar:\"\n      comments: \"Kommentarer:\"\n      votes: \"Stemmer:\"\n      users: \"Brugere:\"\n      flags: \"Anmeldelser:\"\n      reviews: \"Gennemgange:\"\n      site_health: Websteds sundhed\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload mappe:\"\n      run_mode: \"Kørselstilstand:\"\n      private: Privat\n      public: Offentlig\n      smtp: \"SMTP:\"\n      timezone: \"Tidszone:\"\n      system_info: System information\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database størrelse:\"\n      storage_used: \"Anvendt lagerplads:\"\n      uptime: \"Oppetid:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Kontakt os\n      forum: Forum\n      documents: Dokumenter\n      feedback: Tilbagemelding\n      support: Support\n      review: Gennemgå\n      config: Konfiguration\n      update_to: Opdatér til\n      latest: Seneste\n      check_failed: Tjek mislykkedes\n      \"yes\": \"Ja\"\n      \"no\": \"Nej\"\n      not_allowed: Ikke tilladt\n      allowed: Tilladt\n      enabled: Aktiveret\n      disabled: Deaktiveret\n      writable: Skrivbar\n      not_writable: Ikke skrivbar\n    flags:\n      title: Anmeldelser\n      pending: Ventende\n      completed: Gennemført\n      flagged: Anmeldt\n      flagged_type: Anmeldt{{ type }}\n      created: Oprettet\n      action: Handling\n      review: Gennemgå\n    user_role_modal:\n      title: Skift brugerrolle til...\n      btn_cancel: Annuller\n      btn_submit: Indsend\n    new_password_modal:\n      title: Angiv ny adgangskode\n      form:\n        fields:\n          password:\n            label: Adgangskode\n            text: Brugeren vil blive logget ud og skal logge ind igen.\n            msg: Adgangskoden skal være på 8- 32 tegn.\n      btn_cancel: Annuller\n      btn_submit: Indsend\n    edit_profile_modal:\n      title: Rediger profil\n      form:\n        fields:\n          display_name:\n            label: Visnings-navn\n            msg_range: Visningsnavnet skal være 2-30 tegn i længden.\n          username:\n            label: Brugernavn\n            msg_range: Brugernavn skal være 2-30 tegn i længden.\n          email:\n            label: E-mail\n            msg_invalid: Ugyldig E-Mail Adresse.\n      edit_success: Redigering lykkedes\n      btn_cancel: Annuller\n      btn_submit: Indsend\n    user_modal:\n      title: Tilføj ny bruger\n      form:\n        fields:\n          users:\n            label: Masse-tilføj brugere\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Adskil “navn, e-mail, adgangskode” med kommaer. Én bruger pr. linje.\n            msg: \"Indtast venligst brugerens e-mail, en pr. linje.\"\n          display_name:\n            label: Visnings-navn\n            msg: Visningsnavnet skal være 2-30 tegn i længden.\n          email:\n            label: E-mail\n            msg: E-mail er ugyldig.\n          password:\n            label: Adgangskode\n            msg: Adgangskoden skal være 8- 32 tegn.\n      btn_cancel: Annuller\n      btn_submit: Indsend\n    users:\n      title: Brugere\n      name: Navn\n      email: E-mail\n      reputation: Omdømme\n      created_at: Oprettet Tidspunkt\n      delete_at: Slettet Tidspunkt\n      suspend_at: Suspenderet Tidspunkt\n      suspend_until: Suspenderet indtil\n      status: Status\n      role: Rolle\n      action: Handling\n      change: Ændre\n      all: Alle\n      staff: Ansatte\n      more: Mere\n      inactive: Inaktiv\n      suspended: Suspenderet\n      deleted: Slettet\n      normal: Normal\n      Moderator: Moderator\n      Admin: Administrator\n      User: Bruger\n      filter:\n        placeholder: \"Filtrer efter navn, user:id\"\n      set_new_password: Angiv ny adgangskode\n      edit_profile: Rediger profil\n      change_status: Ændre status\n      change_role: Ændre rolle\n      show_logs: Vis logfiler\n      add_user: Tilføj bruger\n      deactivate_user:\n        title: Deaktiver bruger\n        content: En inaktiv bruger skal bekræfte deres e-mail igen.\n      delete_user:\n        title: Slet denne bruger\n        content: Er du sikker på, at du vil slette denne bruger? Dette er permanent!\n        remove: Fjern deres indhold\n        label: Fjern alle spørgsmål, svar, kommentarer osv.\n        text: Tjek ikke dette, hvis du kun ønsker at slette brugerens konto.\n      suspend_user:\n        title: Suspendér denne bruger\n        content: En suspenderet bruger kan ikke logge ind.\n        label: Hvor længe vil brugeren blive suspenderet til?\n        forever: For evigt\n    questions:\n      page_title: Spørgsmål\n      unlisted: Fjernet fra liste\n      post: Indlæg\n      votes: Stemmer\n      answers: Svar\n      created: Oprettet\n      status: Status\n      action: Handling\n      change: Ændre\n      pending: Ventende\n      filter:\n        placeholder: \"Filtrer efter titel, question:id\"\n    answers:\n      page_title: Svar\n      post: Indlæg\n      votes: Stemmer\n      created: Oprettet\n      status: Status\n      action: Handling\n      change: Ændre\n      filter:\n        placeholder: \"Filtrer efter titel, answer:id\"\n    general:\n      page_title: Generelt\n      name:\n        label: Websted navn\n        msg: Websted-navn skal udfyldes.\n        text: \"Navnet på dette websted, som bruges i title-nøgleord.\"\n      site_url:\n        label: Websted URL\n        msg: Websted-URL skal udfyldes.\n        validate: Angiv et gyldigt URL.\n        text: Adressen på dit websted.\n      short_desc:\n        label: Kort beskrivelse af websted\n        msg: Kort beskrivelse af websted skal udfyldes.\n        text: \"Kort beskrivelse, som anvendt i title-nøgleord på hjemmesiden.\"\n      desc:\n        label: Websted beskrivelse\n        msg: Webstedsbeskrivelse skal udfyldes.\n        text: \"Beskriv dette websted i en sætning, som bruges i meta description nøgleord.\"\n      contact_email:\n        label: Kontakt e-mail\n        msg: Kontakt-e-mail skal udfyldes.\n        validate: Kontakt-e-mail er ugyldig.\n        text: E-mailadresse på nøglekontakt ansvarlig for dette websted.\n      check_update:\n        label: Opdatering af software\n        text: Søg automatisk efter opdateringer\n    interface:\n      page_title: Brugerflade\n      language:\n        label: Brugerflade sprog\n        msg: Brugerflade-sprog skal udfyldes.\n        text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden.\n      time_zone:\n        label: Tidszone\n        msg: Tidszone skal udfyldes.\n        text: Vælg en by i samme tidszone som dig selv.\n      avatar:\n        label: Standard avatar\n        text: For brugere uden en brugerdefineret avatar.\n      gravatar_base_url:\n        label: Gravatar base-URL\n        text: URL for Gravatar-udbyderens API-base. Ignoreres når tom.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Fra e-mail\n        msg: Fra e-mail skal udfyldes.\n        text: E-mail-adressen som e-mails sendes fra.\n      from_name:\n        label: Fra navn\n        msg: Fra navn skal udfyldes.\n        text: Navnet som e-mails sendes fra.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host skal udfyldes.\n        text: Din mail-server.\n      encryption:\n        label: Kryptering\n        msg: Kryptering skal udfyldes.\n        text: For de fleste servere er SSL den anbefalede indstilling.\n        ssl: SSL\n        tls: TLS\n        none: Ingen\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port skal være nummer 1 ~ 65535.\n        text: Porten til din mailserver.\n      smtp_username:\n        label: SMTP brugernavn\n        msg: SMTP brugernavn skal udfyldes.\n      smtp_password:\n        label: SMTP adgangskode\n        msg: SMTP adgangskode skal udfyldes.\n      test_email_recipient:\n        label: Test e-mail modtagere\n        text: Angiv e-mail-adresse, der vil modtage test-beskedder.\n        msg: Test e-mail modtagere er ugyldige\n      smtp_authentication:\n        label: Aktiver autentificering\n        title: SMTP autentificering\n        msg: SMTP autentificering skal udfyldes.\n        \"yes\": \"Ja\"\n        \"no\": \"Nej\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo skal udfyldes.\n        text: Logoet billede øverst til venstre på dit websted. Brug et bredt rektangulært billede med en højde på 56 og et breddeforhold større end 3:1. Hvis efterladt tom, vil webstedets titeltekst blive vist.\n      mobile_logo:\n        label: Mobil logo\n        text: Logoet bruges på mobile version af dit websted. Brug et bredt rektangulært billede med en højde på 56. Hvis efterladt tom, vil billedet fra indstillingen \"logo\" blive brugt.\n      square_icon:\n        label: Kvadratisk ikon\n        msg: Kvadratisk ikon skal udfyldes.\n        text: Billede brugt som basis for metadata-ikoner. Bør være større end 512x512.\n      favicon:\n        label: Favicon\n        text: En favicon til dit websted. For at fungere korrekt over en CDN skal det være en png. Vil blive ændret til 32x32. Hvis efterladt tomt, vil \"firkantet ikon\" blive brugt.\n    legal:\n      page_title: Jura\n      terms_of_service:\n        label: Betingelser for brug\n        text: \"Du kan tilføje servicevilkår her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her.\"\n      privacy_policy:\n        label: Privatlivspolitik\n        text: \"Du kan tilføje privatlivspolitik indhold her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her.\"\n      external_content_display:\n        label: Eksternt indhold\n        text: \"Indholdet indeholder billeder, videoer og medier indlejret fra eksterne hjemmesider.\"\n        always_display: Vis altid eksternt indhold\n        ask_before_display: Spørg før visning af eksternt indhold\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum længde for spørgsmål-tekst\n        text: Mindste tilladte spørgsmåls længde i tegn.\n      restrict_answer:\n        title: Skriv svar\n        label: Hver bruger kan kun skrive et svar for det samme spørgsmål\n        text: \"Slå fra for at give brugerne mulighed for at skrive flere svar på det samme spørgsmål, hvilket kan forårsage svar at være ufokuseret.\"\n      min_tags:\n        label: \"Minimumsnøgleord pr. spørgsmål\"\n        text: \"Minimum antal nøgleord kræves i et spørgsmål.\"\n      recommend_tags:\n        label: Anbefal nøgleord\n        text: \"Anbefal nøgleord vil som standard blive vist i dropdown-listen.\"\n        msg:\n          contain_reserved: \"anbefalede nøgleord kan ikke indeholde reserverede tags\"\n      required_tag:\n        title: Angiv påkrævede nøgleord\n        label: Sæt “Anbefal nøgleord” som påkrævede nøgleord\n        text: \"Hvert nyt spørgsmål skal have mindst et anbefalet nøgleord\"\n      reserved_tags:\n        label: Reserverede nøgleord\n        text: \"Reserverede nøgleord kan kun bruges af moderator.\"\n      image_size:\n        label: Maks billedstørrelse (MB)\n        text: \"Den maksimale billedupload størrelse.\"\n      attachment_size:\n        label: Max vedhæftningsstørrelse (MB)\n        text: \"Den maksimale vedhæftede filer upload størrelse.\"\n      image_megapixels:\n        label: Max billed megapixels\n        text: \"Maksimalt antal megapixels tilladt for et billede.\"\n      image_extensions:\n        label: Godkendte billedudvidelser\n        text: \"En liste over tilladte fil udvidelser til billedvisning, adskilt med kommaer.\"\n      attachment_extensions:\n        label: Autoriserede vedhæftningsudvidelser\n        text: \"En liste over fil udvidelser tilladt for upload, adskil med kommaer. ADVARSEL: At tillade uploads kan forårsage sikkerhedsproblemer.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Brugerdefinerede URL-strukturer kan forbedre brugervenlighed og fremadrettet kompatibilitet af dine links.\n      robots:\n        label: robots.txt\n        text: Dette vil permanent tilsidesætte eventuelle relaterede webstedsindstillinger.\n    themes:\n      page_title: Temaer\n      themes:\n        label: Temaer\n        text: Vælg et eksisterende tema.\n      color_scheme:\n        label: Farveskema\n      navbar_style:\n        label: Navigationsbjælke baggrundsstil\n      primary_color:\n        label: Primær farve\n        text: Ændre farver, der bruges af dine temaer\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS og HTML\n      custom_css:\n        label: Brugerdefineret CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Overskrift\n        text: >\n\n      footer:\n        label: Sidefod\n        text: Dette indsættes før &lt;/body>.\n      sidebar:\n        label: Sidebjælke\n        text: Dette vil indsætte i sidebjælken.\n    login:\n      page_title: Log Ind\n      membership:\n        title: Medlemskab\n        label: Tillad nye registreringer\n        text: Slå fra for at forhindre at nogen opretter en ny konto.\n      email_registration:\n        title: E-mail-registrering\n        label: Tillad e-mail registrering\n        text: Slå fra for at forhindre, at der oprettes en ny konto via e-mail.\n      allowed_email_domains:\n        title: Tilladte e-mail-domæner\n        text: E-mail-domæner som brugere skal registrere konti med. Et domæne pr. linje. Ignoreres når tomt.\n      private:\n        title: Privat\n        label: Log ind påkrævet\n        text: Kun brugere som er logget ind har adgang til dette fællesskab.\n      password_login:\n        title: Adgangskode log ind\n        label: Tillad e-mail og adgangskode login\n        text: \"ADVARSEL: Hvis du slår fra, kan du muligvis ikke logge ind, hvis du ikke tidligere har konfigureret en anden loginmetode.\"\n    installed_plugins:\n      title: Installerede Plugins\n      plugin_link: Plugins udvider og udvider funktionaliteten. Du kan finde plugins i <1>Plugin Repository</1>.\n      filter:\n        all: Alle\n        active: Aktiv\n        inactive: Inaktiv\n        outdated: Forældet\n      plugins:\n        label: Plugins\n        text: Vælg et eksisterende plugin.\n      name: Navn\n      version: Version\n      status: Status\n      action: Handling\n      deactivate: Deaktiver\n      activate: Aktivér\n      settings: Indstillinger\n    settings_users:\n      title: Brugere\n      avatar:\n        label: Standard avatar\n        text: For brugere uden en brugerdefineret avatar.\n      gravatar_base_url:\n        label: Gravatar base-URL\n        text: URL for Gravatar-udbyderens API-base. Ignoreres når tom.\n      profile_editable:\n        title: Profil redigerbar\n      allow_update_display_name:\n        label: Tillad brugere at ændre deres visningsnavn\n      allow_update_username:\n        label: Tillad brugere at ændre deres brugernavn\n      allow_update_avatar:\n        label: Tillad brugere at ændre deres profilbillede\n      allow_update_bio:\n        label: Tillad brugere at ændre deres om-mig\n      allow_update_website:\n        label: Tillad brugere at ændre deres hjemmeside\n      allow_update_location:\n        label: Tillad brugere at ændre deres placering\n    privilege:\n      title: Rettigheder\n      level:\n        label: Omdømme påkrævet niveau\n        text: Vælg det omdømme der kræves for rettighederne\n      msg:\n        should_be_number: input skal være et tal\n        number_larger_1: tal skal være lig med eller større end 1\n    badges:\n      action: Handling\n      active: Aktiv\n      activate: Aktiver\n      all: Alle\n      awards: Præmier\n      deactivate: Deaktiver\n      filter:\n        placeholder: Filtrer efter navn, user:id\n      group: Gruppe\n      inactive: Inaktiv\n      name: Navn\n      show_logs: Vis log\n      status: Status\n      title: Emblemer\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (valgfrit)\n    empty: skal udfyldes\n    invalid: er ugyldigt\n    btn_submit: Gem\n    not_found_props: \"Nødvendig egenskab {{ key }} ikke fundet.\"\n    select: Vælg\n  page_review:\n    review: Gennemgå\n    proposed: foreslået\n    question_edit: Rediger spørgsmål\n    answer_edit: Svar redigér\n    tag_edit: Nøgleord redigér\n    edit_summary: Rediger resumé\n    edit_question: Rediger spørgsmål\n    edit_answer: Rediger svar\n    edit_tag: Rediger nøgleord\n    empty: Ingen gennemgangsopgaver tilbage.\n    approve_revision_tip: Godkender du denne revision?\n    approve_flag_tip: Godkender du denne anmeldelse?\n    approve_post_tip: Godkender du dette indlæg?\n    approve_user_tip: Godkender du denne bruger?\n    suggest_edits: Foreslåede redigeringer\n    flag_post: Anmeld indlæg\n    flag_user: Anmeld bruger\n    queued_post: Indlæg i kø\n    queued_user: Brugere i kø\n    filter_label: Type\n    reputation: omdømme\n    flag_post_type: Anmeld dette indlæg som {{ type }}.\n    flag_user_type: Anmeldte dette indlæg som {{ type }}.\n    edit_post: Rediger opslag\n    list_post: Sæt indlæg på liste\n    unlist_post: Fjern indlæg fra liste\n  timeline:\n    undeleted: genskabt\n    deleted: slettet\n    downvote: stem ned\n    upvote: stem op\n    accept: acceptér\n    cancelled: annulleret\n    commented: kommenteret\n    rollback: tilbagerul\n    edited: redigeret\n    answered: besvaret\n    asked: spurgt\n    closed: lukket\n    reopened: genåbnet\n    created: oprettet\n    pin: fastgjort\n    unpin: frigjort\n    show: sat på liste\n    hide: fjernet fra liste\n    title: \"Historik for\"\n    tag_title: \"Tidslinje for\"\n    show_votes: \"Vis stemmer\"\n    n_or_a: Ikke Relevant\n    title_for_question: \"Tidslinje for\"\n    title_for_answer: \"Tidslinje for svar på {{ title }} af {{ author }}\"\n    title_for_tag: \"Tidslinje for nøgleord\"\n    datetime: Datetime\n    type: Type\n    by: Af\n    comment: Kommentar\n    no_data: \"Vi kunne ikke finde noget.\"\n  users:\n    title: Brugere\n    users_with_the_most_reputation: Brugere med det højeste omdømme scorer denne uge\n    users_with_the_most_vote: Brugere, der stemte mest i denne uge\n    staffs: Vores fællesskabs personale\n    reputation: omdømme\n    votes: stemmer\n  prompt:\n    leave_page: Er du sikker på, at du vil forlade siden?\n    changes_not_save: Dine ændringer er muligvis ikke gemt.\n  draft:\n    discard_confirm: Er du sikker på, at du vil kassere dit udkast?\n  messages:\n    post_deleted: Dette indlæg er blevet slettet.\n    post_cancel_deleted: Dette opslag er blevet genoprettet.\n    post_pin: Dette indlæg er blevet fastgjort.\n    post_unpin: Dette indlæg er blevet frigjort.\n    post_hide_list: Dette indlæg er blevet skjult fra listen.\n    post_show_list: Dette indlæg er blevet vist på listen.\n    post_reopen: Dette indlæg er blevet genåbnet.\n    post_list: Dette indlæg er blevet listet.\n    post_unlist: Dette indlæg er blevet aflistet.\n    post_pending: Dit indlæg afventer gennemgang. Dette er en forhåndsvisning, det vil være synligt, når det er blevet godkendt.\n    post_closed: Dette opslag er blebet lukket.\n    answer_deleted: Dette svar er blevet slettet.\n    answer_cancel_deleted: Dette svar er blevet genoprettet.\n    change_user_role: Denne brugers rolle er blevet ændret.\n    user_inactive: Denne bruger er allerede inaktiv.\n    user_normal: Denne bruger er allerede normal.\n    user_suspended: Denne bruger er blevet suspenderet.\n    user_deleted: Denne bruger er slettet.\n    user_added: User has been added successfully.\n    badge_activated: Dette emblem er blevet aktiveret.\n    badge_inactivated: Dette emblem er blevet inaktiveret.\n    users_deleted: Disse bruger er blevet slettet.\n    posts_deleted: Disse spørgsmål er blevet slettet.\n    answers_deleted: Disse svar er blevet slettet.\n    copy: Kopier til udklipsholder\n    copied: Kopieret\n    external_content_warning: Eksterne billeder/medier vises ikke.\n\n\n"
  },
  {
    "path": "i18n/de_DE.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Erfolgreich.\n    unknown:\n      other: Unbekannter Fehler.\n    request_format_error:\n      other: Format der Anfrage ist ungültig.\n    unauthorized_error:\n      other: Nicht autorisiert.\n    database_error:\n      other: Datenbank-Fehler.\n    forbidden_error:\n      other: Verboten.\n    duplicate_request_error:\n      other: Doppelte Einreichung.\n  action:\n    report:\n      other: Melden\n    edit:\n      other: Bearbeiten\n    delete:\n      other: Löschen\n    close:\n      other: Schließen\n    reopen:\n      other: Wieder öffnen\n    forbidden_error:\n      other: Verboten.\n    pin:\n      other: Anpinnen\n    hide:\n      other: Von Liste nehmen\n    unpin:\n      other: Loslösen\n    show:\n      other: Liste\n    invite_someone_to_answer:\n      other: Bearbeiten\n    undelete:\n      other: Wiederherstellen\n    merge:\n      other: Zusammenführen\n  role:\n    name:\n      user:\n        other: Benutzer\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Standard ohne speziellen Zugriff.\n      admin:\n        other: Habe die volle Berechtigung, auf die Seite zuzugreifen.\n      moderator:\n        other: Hat Zugriff auf alle Beiträge außer Admin-Einstellungen.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (weniger Reputation für privates Team, Gruppen)\n    level_2:\n      description:\n        other: Level 2 (niedrige Reputation für Startup-Community)\n    level_3:\n      description:\n        other: Level 3 (hohe Reputation für eine reife Community)\n    level_custom:\n      description:\n        other: Benutzerdefinierter Level\n    rank_question_add_label:\n      other: Fragen stellen\n    rank_answer_add_label:\n      other: Antwort schreiben\n    rank_comment_add_label:\n      other: Kommentar schreiben\n    rank_report_add_label:\n      other: Melden\n    rank_comment_vote_up_label:\n      other: Kommentar upvoten\n    rank_link_url_limit_label:\n      other: Mehr als 2 Links gleichzeitig posten\n    rank_question_vote_up_label:\n      other: Frage upvoten\n    rank_answer_vote_up_label:\n      other: Antwort upvoten\n    rank_question_vote_down_label:\n      other: Frage downvoten\n    rank_answer_vote_down_label:\n      other: Antwort downvoten\n    rank_invite_someone_to_answer_label:\n      other: Jemanden zum Antworten einladen\n    rank_tag_add_label:\n      other: Neuen Tag erstellen\n    rank_tag_edit_label:\n      other: Tag-Beschreibung bearbeiten (muss überprüft werden)\n    rank_question_edit_label:\n      other: Frage eines anderen bearbeiten (muss überarbeitet werden)\n    rank_answer_edit_label:\n      other: Antwort eines anderen bearbeiten (muss überarbeitet werden)\n    rank_question_edit_without_review_label:\n      other: Frage eines anderen ohne Überprüfung bearbeiten\n    rank_answer_edit_without_review_label:\n      other: Antwort eines anderen ohne Überprüfung bearbeiten\n    rank_question_audit_label:\n      other: Frageänderungen überprüfen\n    rank_answer_audit_label:\n      other: Bearbeitete Antworten überprüfen\n    rank_tag_audit_label:\n      other: Tag-Bearbeitungen überprüfen\n    rank_tag_edit_without_review_label:\n      other: Tag-Beschreibung ohne Überprüfung bearbeiten\n    rank_tag_synonym_label:\n      other: Tag-Synonyme verwalten\n  email:\n    other: E-Mail\n  e_mail:\n    other: E-Mail\n  password:\n    other: Passwort\n  pass:\n    other: Passwort\n  old_pass:\n    other: Aktuelles Passwort\n  original_text:\n    other: Dieser Beitrag\n  email_or_password_wrong_error:\n    other: E-Mail und Passwort stimmen nicht überein.\n  error:\n    common:\n      invalid_url:\n        other: Ungültige URL.\n      status_invalid:\n        other: Ungültiger Status.\n    password:\n      space_invalid:\n        other: Passwort darf keine Leerzeichen enthalten.\n    admin:\n      cannot_update_their_password:\n        other: Du kannst dein Passwort nicht ändern.\n      cannot_edit_their_profile:\n        other: Du kannst dein Profil nicht bearbeiten.\n      cannot_modify_self_status:\n        other: Du kannst deinen Status nicht ändern.\n      email_or_password_wrong:\n        other: E-Mail und Password stimmen nicht überein.\n    answer:\n      not_found:\n        other: Antwort nicht gefunden.\n      cannot_deleted:\n        other: Keine Berechtigung zum Löschen.\n      cannot_update:\n        other: Keine Berechtigung zum Aktualisieren.\n      question_closed_cannot_add:\n        other: Fragen sind geschlossen und können nicht hinzugefügt werden.\n      content_cannot_empty:\n        other: Die Antwort darf nicht leer sein.\n    comment:\n      edit_without_permission:\n        other: Kommentar kann nicht bearbeitet werden.\n      not_found:\n        other: Kommentar wurde nicht gefunden.\n      cannot_edit_after_deadline:\n        other: Die Kommentarzeit war zu lang, um sie zu ändern.\n      content_cannot_empty:\n        other: Der Kommentar darf nicht leer sein.\n    email:\n      duplicate:\n        other: E-Mail existiert bereits.\n      need_to_be_verified:\n        other: E-Mail muss überprüft werden.\n      verify_url_expired:\n        other: Die verifizierbare E-Mail-URL ist abgelaufen, bitte sende die E-Mail erneut.\n      illegal_email_domain_error:\n        other: E-Mails sind von dieser E-Mail-Domäne nicht erlaubt. Bitte verwende eine andere.\n    lang:\n      not_found:\n        other: Sprachdatei nicht gefunden.\n    object:\n      captcha_verification_failed:\n        other: Captcha ist falsch.\n      disallow_follow:\n        other: Es ist dir nicht erlaubt zu folgen.\n      disallow_vote:\n        other: Es ist dir nicht erlaubt abzustimmen.\n      disallow_vote_your_self:\n        other: Du kannst nicht für deinen eigenen Beitrag stimmen.\n      not_found:\n        other: Objekt nicht gefunden.\n      verification_failed:\n        other: Verifizierung fehlgeschlagen.\n      email_or_password_incorrect:\n        other: E-Mail und Passwort stimmen nicht überein.\n      old_password_verification_failed:\n        other: Die Überprüfung des alten Passworts ist fehlgeschlagen\n      new_password_same_as_previous_setting:\n        other: Das neue Passwort ist das gleiche wie das vorherige Passwort.\n      already_deleted:\n        other: Dieser Beitrag wurde gelöscht.\n    meta:\n      object_not_found:\n        other: Metaobjekt nicht gefunden\n    question:\n      already_deleted:\n        other: Dieser Beitrag wurde gelöscht.\n      under_review:\n        other: Ihr Beitrag wartet auf Überprüfung. Er wird sichtbar sein, nachdem er genehmigt wurde.\n      not_found:\n        other: Frage nicht gefunden.\n      cannot_deleted:\n        other: Keine Berechtigung zum Löschen.\n      cannot_close:\n        other: Keine Berechtigung zum Schließen.\n      cannot_update:\n        other: Keine Berechtigung zum Aktualisieren.\n      content_cannot_empty:\n        other: Der Inhalt darf nicht leer sein.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Ansehenssrang erfüllt die Bedingung nicht.\n      vote_fail_to_meet_the_condition:\n        other: Danke für dein Feedback. Du brauchst mindestens {{.Rank}} Ansehen, um eine Stimme abzugeben.\n      no_enough_rank_to_operate:\n        other: Dafür brauchst du mindestens {{.Rank}} Ansehen.\n    report:\n      handle_failed:\n        other: Bearbeiten der Meldung fehlgeschlagen.\n      not_found:\n        other: Meldung nicht gefunden.\n    tag:\n      already_exist:\n        other: Tag existiert bereits.\n      not_found:\n        other: Tag nicht gefunden.\n      recommend_tag_not_found:\n        other: Das Tag \"Empfehlen\" ist nicht vorhanden.\n      recommend_tag_enter:\n        other: Bitte gib mindestens einen erforderlichen Tag ein.\n      not_contain_synonym_tags:\n        other: Sollte keine Synonym-Tags enthalten.\n      cannot_update:\n        other: Keine Berechtigung zum Aktualisieren.\n      is_used_cannot_delete:\n        other: Du kannst keinen Tag löschen, der in Gebrauch ist.\n      cannot_set_synonym_as_itself:\n        other: Du kannst das Synonym des aktuellen Tags nicht als sich selbst festlegen.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Der Absendername kann keine E-Mail-Adresse sein.\n    theme:\n      not_found:\n        other: Design nicht gefunden.\n    revision:\n      review_underway:\n        other: Kann derzeit nicht bearbeitet werden, es existiert eine Version in der Überprüfungswarteschlange.\n      no_permission:\n        other: Keine Berechtigung zum Überarbeiten.\n    user:\n      external_login_missing_user_id:\n        other: Die Plattform des Drittanbieters stellt keine eindeutige UserID zur Verfügung, sodass du dich nicht anmelden kannst. Bitte wende dich an den Administrator der Website.\n      external_login_unbinding_forbidden:\n        other: Bitte setze ein Login-Passwort für dein Konto, bevor du dieses Login entfernst.\n      email_or_password_wrong:\n        other:\n          other: E-Mail und Passwort stimmen nicht überein.\n      not_found:\n        other: Benutzer nicht gefunden.\n      suspended:\n        other: Benutzer wurde gesperrt.\n      username_invalid:\n        other: Benutzername ist ungültig.\n      username_duplicate:\n        other: Benutzername wird bereits verwendet.\n      set_avatar:\n        other: Avatar setzen fehlgeschlagen.\n      cannot_update_your_role:\n        other: Du kannst deine Rolle nicht ändern.\n      not_allowed_registration:\n        other: Derzeit ist die Seite nicht für die Anmeldung geöffnet.\n      not_allowed_login_via_password:\n        other: Zurzeit ist es auf der Seite nicht möglich, sich mit einem Passwort anzumelden.\n      access_denied:\n        other: Zugriff verweigert\n      page_access_denied:\n        other: Du hast keinen Zugriff auf diese Seite.\n      add_bulk_users_format_error:\n        other: \"Fehler {{.Field}}-Format in der Nähe von '{{.Content}}' in Zeile {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Die Anzahl der Benutzer, die du auf einmal hinzufügst, sollte im Bereich von 1-{{.MaxAmount}} liegen.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Lesekonfiguration fehlgeschlagen\n    database:\n      connection_failed:\n        other: Datenbankverbindung fehlgeschlagen\n      create_table_failed:\n        other: Tabelle erstellen fehlgeschlagen\n    install:\n      create_config_failed:\n        other: Kann die config.yaml-Datei nicht erstellen.\n    upload:\n      unsupported_file_format:\n        other: Dateiformat nicht unterstützt.\n    site_info:\n      config_not_found:\n        other: Seiten-Konfiguration nicht gefunden.\n    badge:\n      object_not_found:\n        other: Abzeichen-Objekt nicht gefunden\n  reason:\n    spam:\n      name:\n        other: Spam\n      desc:\n        other: Dieser Beitrag ist eine Werbung oder Vandalismus. Er ist nicht nützlich oder relevant für das aktuelle Thema.\n    rude_or_abusive:\n      name:\n        other: unhöflich oder beleidigend\n      desc:\n        other: \"Eine vernünftige Person würde diesen Inhalt im respektvoll diskutierten Diskurs für unangemessen halten.\"\n    a_duplicate:\n      name:\n        other: ein Duplikat\n      desc:\n        other: Diese Frage wurde schon einmal gestellt und hat bereits eine Antwort.\n      placeholder:\n        other: Gib den Link zur bestehenden Frage ein\n    not_a_answer:\n      name:\n        other: keine Antwort\n      desc:\n        other: \"Die Antwort versucht nicht, die Frage zu beantworten. Sie sollte entweder bearbeitet, kommentiert, als weitere Frage gestellt oder ganz gelöscht werden.\"\n    no_longer_needed:\n      name:\n        other: nicht mehr benötigt\n      desc:\n        other: Dieser Kommentar ist veraltet oder nicht relevant für diesen Beitrag.\n    something:\n      name:\n        other: anderer Grund\n      desc:\n        other: Dieser Beitrag erfordert die Aufmerksamkeit der Temmitglieder aus einem anderen, oben nicht genannten Grund.\n      placeholder:\n        other: Lass uns wissen, worüber du dir Sorgen machst\n    community_specific:\n      name:\n        other: ein Community-spezifischer Grund\n      desc:\n        other: Diese Frage entspricht nicht den Gemeinschaftsrichtlinien.\n    not_clarity:\n      name:\n        other: benötigt Details oder Klarheit\n      desc:\n        other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren.\n    looks_ok:\n      name:\n        other: sieht OK aus\n      desc:\n        other: Dieser Beitrag ist gut so wie er ist und nicht von schlechter Qualität.\n    needs_edit:\n      name:\n        other: muss bearbeitet werden, und ich habe es getan\n      desc:\n        other: Verbessere und korrigiere Probleme mit diesem Beitrag selbst.\n    needs_close:\n      name:\n        other: muss geschlossen werden\n      desc:\n        other: Eine geschlossene Frage kann nicht beantwortet werden, aber du kannst sie trotzdem bearbeiten, abstimmen und kommentieren.\n    needs_delete:\n      name:\n        other: muss gelöscht werden\n      desc:\n        other: Dieser Beitrag wird gelöscht.\n  question:\n    close:\n      duplicate:\n        name:\n          other: Spam\n        desc:\n          other: Diese Frage ist bereits gestellt worden und hat bereits eine Antwort.\n      guideline:\n        name:\n          other: ein Community-spezifischer Grund\n        desc:\n          other: Diese Frage entspricht nicht einer Gemeinschaftsrichtlinie.\n      multiple:\n        name:\n          other: benötigt Details oder Klarheit\n        desc:\n          other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren.\n      other:\n        name:\n          other: etwas anderes\n        desc:\n          other: Dieser Beitrag erfordert einen anderen Grund, der oben nicht aufgeführt ist.\n    operation_type:\n      asked:\n        other: gefragt\n      answered:\n        other: beantwortet\n      modified:\n        other: geändert\n    deleted_title:\n      other: Gelöschte Frage\n    questions_title:\n      other: Fragen\n  tag:\n    tags_title:\n      other: Schlagwörter\n    no_description:\n      other: Diese Kategorie hat keine Beschreibung.\n  notification:\n    action:\n      update_question:\n        other: aktualisierte Frage\n      answer_the_question:\n        other: beantwortete Frage\n      update_answer:\n        other: aktualisierte Antwort\n      accept_answer:\n        other: akzeptierte Antwort\n      comment_question:\n        other: kommentierte Frage\n      comment_answer:\n        other: kommentierte Antwort\n      reply_to_you:\n        other: hat Ihnen geantwortet\n      mention_you:\n        other: hat dich erwähnt\n      your_question_is_closed:\n        other: Deine Frage wurde geschlossen\n      your_question_was_deleted:\n        other: Deine Frage wurde gelöscht\n      your_answer_was_deleted:\n        other: Deine Antwort wurde gelöscht\n      your_comment_was_deleted:\n        other: Dein Kommentar wurde gelöscht\n      up_voted_question:\n        other: positiv bewertete Frage\n      down_voted_question:\n        other: negativ bewertete Frage\n      up_voted_answer:\n        other: positiv bewertete Antwort\n      down_voted_answer:\n        other: negativ bewertete Antwort\n      up_voted_comment:\n        other: positiv bewerteter Kommentar\n      invited_you_to_answer:\n        other: hat dich eingeladen, zu antworten\n      earned_badge:\n        other: Du hast das \"{{.BadgeName}}\" Abzeichen verdient\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Bestätige deine neue E-Mail-Adresse\"\n      body:\n        other: \"Bestätigen Sie Ihre neue E-Mail-Adresse für {{.SiteName}} indem Sie auf den folgenden Link klicken:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nWenn Sie diese Änderung nicht angefordert haben bitte diese E-Mail ignorieren.<br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} hat deine Frage beantwortet\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n\\n<a href='{{.AnswerUrl}}'>Auf {{.SiteName}} anschauen</a><br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abmelden</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} hat dich eingeladen zu antworten\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Ich denke, Sie kennen die Antwort.</blockquote><br>\\n<a href='{{.InviteUrl}}'> {{.SiteName}}</a><br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abmelden</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} hat deinen Beitrag kommentiert\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n\\n<a href='{{.CommentUrl}}'>Auf {{.SiteName}} anschauen</a><br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abmelden</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Neue Frage: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische Systemnachricht, bitte antworten Sie nicht darauf. Antworten werden nicht gelesen oder bearbeitet.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Benachrichtigung abbestellen</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Passwort zurücksetzen\"\n      body:\n        other: \"Jemand bat darum, Ihr Passwort auf {{.SiteName}}zurückzusetzen.<br><br>\\n\\nWenn Sie es nicht waren, können Sie diese E-Mail sicher ignorieren.<br><br>\\n\\nKlicken Sie auf den folgenden Link, um ein neues Passwort auszuwählen:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n\\n<br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Bestätige dein neues Konto\"\n      body:\n        other: \"Willkommen in {{.SiteName}}!<br><br>\\n\\nKlicken Sie auf den folgenden Link, um Ihr neues Konto zu bestätigen und zu aktivieren:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nWenn der obige Link nicht anklickbar ist kopieren und in die Adressleiste Ihres Webbrowsers einfügen.\\n<br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht sichtbar ist.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test-E-Mail\"\n      body:\n        other: \"Dies ist eine Test-E-Mail.\\n<br><br>\\n\\n--<br>\\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.\"\n  action_activity_type:\n    upvote:\n      other: positiv bewerten\n    upvoted:\n      other: positiv bewertet\n    downvote:\n      other: negativ bewerten\n    downvoted:\n      other: negativ bewertet\n    accept:\n      other: akzeptieren\n    accepted:\n      other: akzeptiert\n    edit:\n      other: bearbeiten\n  review:\n    queued_post:\n      other: Post in der Warteschlange\n    flagged_post:\n      other: Beiträge gemeldet\n    suggested_post_edit:\n      other: Änderungsvorschläge\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} Und {{ .Count }} mehr...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiograph\n        desc:\n          other: Gefüllt mit <a href=\"{{ .ProfileURL }}\" target=\"_blank\">Profil</a> Informationen.\n      certified:\n        name:\n          other: Zertifiziert\n        desc:\n          other: Erledigte unser neues Benutzerhandbuch.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: Erster Beitrag bearbeiten.\n      first_flag:\n        name:\n          other: Erste Meldung\n        desc:\n          other: Erste Meldung eines Beitrags.\n      first_upvote:\n        name:\n          other: Erster Upvote\n        desc:\n          other: Erste Like eines Beitrags.\n      first_link:\n        name:\n          other: Erster Link\n        desc:\n          other: Hat erstmals einen Link zu einem anderen Beitrag hinzugefügt.\n      first_reaction:\n        name:\n          other: Erste Reaktion\n        desc:\n          other: Zuerst reagierte auf den Beitrag.\n      first_share:\n        name:\n          other: Erstes Teilen\n        desc:\n          other: Zuerst einen Beitrag geteilt.\n      scholar:\n        name:\n          other: Gelehrter\n        desc:\n          other: Hat eine Frage gestellt und eine Antwort akzeptiert.\n      commentator:\n        name:\n          other: Kommentator\n        desc:\n          other: Hinterlassen Sie 5 Kommentare.\n      new_user_of_the_month:\n        name:\n          other: Neuer Benutzer des Monats\n        desc:\n          other: Ausstehende Beiträge in ihrem ersten Monat.\n      read_guidelines:\n        name:\n          other: Lesen Sie die Richtlinien\n        desc:\n          other: Lesen Sie die [Community-Richtlinien].\n      reader:\n        name:\n          other: Leser\n        desc:\n          other: Lesen Sie alle Antworten in einem Thema mit mehr als 10 Antworten.\n      welcome:\n        name:\n          other: Willkommen\n        desc:\n          other: Du hast eine positive Abstimmung erhalten.\n      nice_share:\n        name:\n          other: Schöne teilen\n        desc:\n          other: Hat einen Beitrag mit 25 einzigartigen Besuchern freigegeben.\n      good_share:\n        name:\n          other: Gut geteilt\n        desc:\n          other: Hat einen Beitrag mit 300 einzigartigen Besuchern freigegeben.\n      great_share:\n        name:\n          other: Großartiges Teilen\n        desc:\n          other: Hat einen Beitrag mit 1000 einzigartigen Besuchern freigegeben.\n      out_of_love:\n        name:\n          other: Aus Liebe\n        desc:\n          other: Hat an einem Tag 50 Upvotes verwendet.\n      higher_love:\n        name:\n          other: Höhere Liebe\n        desc:\n          other: Hat an einem Tag 50 Upvotes 5 Mal verwendet.\n      crazy_in_love:\n        name:\n          other: Im siebten Himmel\n        desc:\n          other: Hat an einem Tag 50 Upvotes 20 Mal verwendet.\n      promoter:\n        name:\n          other: Förderer\n        desc:\n          other: Hat einen Benutzer eingeladen.\n      campaigner:\n        name:\n          other: Kampagnenleiter\n        desc:\n          other: Lade 3 einfache Benutzer ein.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Hat 5 Mitglieder eingeladen.\n      thank_you:\n        name:\n          other: Vielen Dank\n        desc:\n          other: Beitrag mit 20 Upvotes und 10 abgegebenen Upvotes.\n      gives_back:\n        name:\n          other: Feedback geben\n        desc:\n          other: Beitrag mit 100 Upvotes und 100 abgegebenen Upvotes.\n      empathetic:\n        name:\n          other: Einfühlsam\n        desc:\n          other: Beitrag mit 500 Upvotes und 1000 abgegebenen Upvotes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Besucht 10 aufeinander folgende Tage.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Besucht 100 aufeinander folgende Tage.\n      devotee:\n        name:\n          other: Anhänger\n        desc:\n          other: 365 aufeinander folgende Tage besucht.\n      anniversary:\n        name:\n          other: Jahrestag\n        desc:\n          other: Aktives Mitglied für ein Jahr, mindestens einmal veröffentlicht.\n      appreciated:\n        name:\n          other: Gewertschätzt\n        desc:\n          other: Erhalten 1 up vote für 20 posts.\n      respected:\n        name:\n          other: Respektiert\n        desc:\n          other: Erhalten 2 up vote für 100 posts.\n      admired:\n        name:\n          other: Bewundert\n        desc:\n          other: 5 upvotes für 300 posts erhalten.\n      solved:\n        name:\n          other: Gelöst\n        desc:\n          other: Eine Antwort wurde akzeptiert.\n      guidance_counsellor:\n        name:\n          other: Anleitungsberater\n        desc:\n          other: 10 Antworten wurden akzeptiert.\n      know_it_all:\n        name:\n          other: Alleswisser\n        desc:\n          other: 50 Antworten wurden akzeptiert.\n      solution_institution:\n        name:\n          other: Lösungsfinder\n        desc:\n          other: 150 Antworten wurden akzeptiert.\n      nice_answer:\n        name:\n          other: Nette Antwort\n        desc:\n          other: Die Antwortpunktzahl beträgt mehr als 10 Punkte.\n      good_answer:\n        name:\n          other: Gute Antwort\n        desc:\n          other: Die Antwortpunktzahl beträgt mehr als 25 Punkte.\n      great_answer:\n        name:\n          other: Großartige Antwort\n        desc:\n          other: Die Antwortpunktzahl beträgt mehr als 50 Punkte.\n      nice_question:\n        name:\n          other: Schöne Frage\n        desc:\n          other: Fragenpunktzahl von 10 oder mehr.\n      good_question:\n        name:\n          other: Gute Frage\n        desc:\n          other: Fragen mit 25 oder mehr Punkten.\n      great_question:\n        name:\n          other: Große Frage\n        desc:\n          other: Frage mit 50 oder mehr Punkten.\n      popular_question:\n        name:\n          other: Populäre Frage\n        desc:\n          other: Frage mit 500 Ansichten.\n      notable_question:\n        name:\n          other: Bemerkenswerte Frage\n        desc:\n          other: Frage mit 1.000 Ansichten.\n      famous_question:\n        name:\n          other: Erstklassige Frage\n        desc:\n          other: Frage mit 5.000 Ansichten.\n      popular_link:\n        name:\n          other: Populärer Link\n        desc:\n          other: Hat einen externen Link mit 50 Klicks gepostet.\n      hot_link:\n        name:\n          other: Heißer Link\n        desc:\n          other: Geschrieben einen externen Link mit 300 Klicks.\n      famous_link:\n        name:\n          other: Berühmter Link\n        desc:\n          other: Geschrieben einen externen Link mit 100 Klicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Erste Schritte\n      community:\n        name:\n          other: Gemeinschaft\n      posting:\n        name:\n          other: Freigeben\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Wie man formatiert\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">einen Beitrag erwähnen: <code>#post_id</code></p></li><li><p class=\"mb-2\">um Links</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Titel](https://url.com)</code></pre></li><li><p class=\"mb-2\">Zwischen den Absätzen Zeilenumbrüche einfügen</p></li><li><p class=\"mb-2\"><em>_italic_</em> oder **<strong>fett</strong>**</p></li><li><p class=\"mb-2\">Code um 4 Leerzeichen einrücken</p></li><li><p class=\"mb-2\">Zitat durch Setzen von <code>&gt; </code> am Anfang der Zeile</p></li><li><p class=\"mb-2\">Backtick-Escapes <code>`wie _this_`</code></p></li><li><p class=\"mb-2\">Codeumrandungen mit Backticks <code>`</code></p><pre class=\"mb-0\"><code>`<br/>Code hier<br/>``</code></pre></li></ul>\n  pagination:\n    prev: Zurück\n    next: Weiter\n  page_title:\n    question: Frage\n    questions: Fragen\n    tag: Schlagwort\n    tags: Schlagwörter\n    tag_wiki: tag Wiki\n    create_tag: Tag erstellen\n    edit_tag: Tag bearbeiten\n    ask_a_question: Create Question\n    edit_question: Frage bearbeiten\n    edit_answer: Antwort bearbeiten\n    search: Suchen\n    posts_containing: Beiträge enthalten\n    settings: Einstellungen\n    notifications: Benachrichtigungen\n    login: Anmelden\n    sign_up: Registrieren\n    account_recovery: Konto-Wiederherstellung\n    account_activation: Account Aktivierung\n    confirm_email: Bestätigungs-E-Mail\n    account_suspended: Konto gesperrt\n    admin: Verwaltung\n    change_email: E-Mails ändern\n    install: Installation beantworten\n    upgrade: Antwort-Upgrade\n    maintenance: Website-Wartung\n    users: Benutzer\n    oauth_callback: In Bearbeitung\n    http_404: HTTP-Fehler 404\n    http_50X: HTTP-Fehler 500\n    http_403: HTTP Fehler 403\n    logout: Ausloggen\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Benachrichtigungen\n    inbox: Posteingang\n    achievement: Erfolge\n    new_alerts: Neue Benachrichtigungen\n    all_read: Alle als gelesen markieren\n    show_more: Mehr anzeigen\n    someone: Jemand\n    inbox_type:\n      all: Alle\n      posts: Beiträge\n      invites: Einladungen\n      votes: Abstimmungen\n    answer: Antwort\n    question: Frage\n    badge_award: Abzeichen\n  suspended:\n    title: Dein Konto wurde gesperrt\n    until_time: \"Dein Konto wurde bis zum {{ time }} gesperrt.\"\n    forever: Dieser Benutzer wurde für immer gesperrt.\n    end: Du erfüllst keine Community-Richtlinie.\n    contact_us: Kontaktiere uns\n  editor:\n    blockquote:\n      text: Blockzitat\n    bold:\n      text: Stark\n    chart:\n      text: Bestenliste\n      flow_chart: Flussdiagramm\n      sequence_diagram: Sequenzdiagramm\n      class_diagram: Klassen Diagramm\n      state_diagram: Zustandsdiagramm\n      entity_relationship_diagram: Entitätsbeziehungsdiagramm\n      user_defined_diagram: Benutzerdefiniertes Diagramm\n      gantt_chart: Gantt-Diagramm\n      pie_chart: Kuchendiagramm\n    code:\n      text: Code Beispiel\n      add_code: Code-Beispiel hinzufügen\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code kann nicht leer sein.\n          language:\n            label: Sprache\n            placeholder: Automatische Erkennung\n      btn_cancel: Abbrechen\n      btn_confirm: Hinzufügen\n    formula:\n      text: Formel\n      options:\n        inline: Inline Formel\n        block: Block Formel\n    heading:\n      text: Überschrift\n      options:\n        h1: Überschrift 1\n        h2: Überschrift 2\n        h3: Überschrift 3\n        h4: Überschrift 4\n        h5: Überschrift 5\n        h6: Überschrift 6\n    help:\n      text: Hilfe\n    hr:\n      text: Horizontale Richtlinie\n    image:\n      text: Bild\n      add_image: Bild hinzufügen\n      tab_image: Bild hochladen\n      form_image:\n        fields:\n          file:\n            label: Bilddatei\n            btn: Bild auswählen\n            msg:\n              empty: Datei darf nicht leer sein.\n              only_image: Nur Bilddateien sind erlaubt.\n              max_size: Dateigröße darf {{size}} MB nicht überschreiten.\n          desc:\n            label: Beschreibung\n      tab_url: Bild URL\n      form_url:\n        fields:\n          url:\n            label: Bild URL\n            msg:\n              empty: Bild-URL darf nicht leer sein.\n          name:\n            label: Beschreibung\n      btn_cancel: Abbrechen\n      btn_confirm: Hinzufügen\n      uploading: Hochladen\n    indent:\n      text: Einzug\n    outdent:\n      text: Ausrücken\n    italic:\n      text: Hervorhebung\n    link:\n      text: Hyperlink\n      add_link: Hyperlink hinzufügen\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL darf nicht leer sein.\n          name:\n            label: Beschreibung\n      btn_cancel: Abbrechen\n      btn_confirm: Hinzufügen\n    ordered_list:\n      text: Nummerierte Liste\n    unordered_list:\n      text: Aufzählungsliste\n    table:\n      text: Tabelle\n      heading: Überschrift\n      cell: Zelle\n    file:\n      text: Datei anhängen\n      not_supported: \"Diesen Dateityp nicht unterstützen. Versuchen Sie es erneut mit {{file_type}}.\"\n      max_size: \"Dateigröße anhängen darf {{size}} MB nicht überschreiten.\"\n  close_modal:\n    title: Ich schließe diesen Beitrag als...\n    btn_cancel: Abbrechen\n    btn_submit: Senden\n    remark:\n      empty: Kann nicht leer sein.\n    msg:\n      empty: Bitte wähle einen Grund aus.\n  report_modal:\n    flag_title: Ich melde diesen Beitrag als...\n    close_title: Ich schließe diesen Beitrag wegen ...\n    review_question_title: Frage prüfen\n    review_answer_title: Antwort prüfen\n    review_comment_title: Kommentar prüfen\n    btn_cancel: Abbrechen\n    btn_submit: Senden\n    remark:\n      empty: Kann nicht leer sein.\n    msg:\n      empty: Bitte wähle einen Grund aus.\n      not_a_url: URL hat ein falsches Format.\n      url_not_match: URL-Ursprung stimmt nicht mit der aktuellen Website überein.\n  tag_modal:\n    title: Neuen Tag erstellen\n    form:\n      fields:\n        display_name:\n          label: Anzeigename\n          msg:\n            empty: Anzeigename darf nicht leer sein.\n            range: Anzeige des Namens mit bis zu 35 Zeichen.\n        slug_name:\n          label: URL-Slug\n          desc: 'Muss den Zeichensatz \"a-z\", \"0-9\", \"+ # - \" verwenden.'\n          msg:\n            empty: URL-Slug darf nicht leer sein.\n            range: URL-Slug mit bis zu 35 Zeichen.\n            character: URL-Slug enthält nicht erlaubten Zeichensatz.\n        desc:\n          label: Beschreibung\n        revision:\n          label: Version\n        edit_summary:\n          label: Zusammenfassung bearbeiten\n          placeholder: >-\n            Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung)\n    btn_cancel: Abbrechen\n    btn_submit: Senden\n    btn_post: Neuen Tag erstellen\n  tag_info:\n    created_at: Erstellt\n    edited_at: Bearbeitet\n    history: Verlauf\n    synonyms:\n      title: Synonyme\n      text: Die folgenden Tags werden neu zugeordnet zu\n      empty: Keine Synonyme gefunden.\n      btn_add: Synonym hinzufügen\n      btn_edit: Bearbeiten\n      btn_save: Speichern\n    synonyms_text: Die folgenden Tags werden neu zugeordnet zu\n    delete:\n      title: Diesen Tag löschen\n      tip_with_posts: >-\n        <p>Wir erlauben es nicht, <strong>Tags mit Beiträgen</strong>zu löschen.</p> <p>Bitte entfernen Sie dieses Tag zuerst aus den Beiträgen.</p>\n      tip_with_synonyms: >-\n        <p>Wir erlauben nicht <strong>Tags mit Synonymen</strong>zu löschen.</p> <p>Bitte entfernen Sie zuerst die Synonyme von diesem Schlagwort.</p>\n      tip: Bist du sicher, dass du löschen möchtest?\n      close: Schließen\n    merge:\n      title: Tags zusammenführen\n      source_tag_title: Quell-Tag\n      source_tag_description: Das Quell-Tag und seine zugehörigen Daten werden dem Ziel-Tag zugeordnet.\n      target_tag_title: Ziel-Tag\n      target_tag_description: Ein Synonym zwischen diesen beiden Tags wird nach dem Zusammenführen erstellt.\n      no_results: Keine zusammenpassenden Tags gefunden\n      btn_submit: Absenden\n      btn_close: Schließen\n  edit_tag:\n    title: Tag bearbeiten\n    default_reason: Tag bearbeiten\n    default_first_reason: Tag hinzufügen\n    btn_save_edits: Änderungen speichern\n    btn_cancel: Abbrechen\n  dates:\n    long_date: DD. MMM\n    long_date_with_year: \"DD. MMM YYYY\"\n    long_date_with_time: \"DD. MMM YYYY [at] HH:mm\"\n    now: Gerade eben\n    x_seconds_ago: \"Vor {{count}}s\"\n    x_minutes_ago: \"Vor {{count}}m\"\n    x_hours_ago: \"Vor {{count}}h\"\n    hour: Stunde\n    day: tag\n    hours: Stunden\n    days: Tage\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: Herz\n    smile: Lächeln\n    frown: Stirnrunzeln\n    btn_label: Reaktionen hinzufügen oder entfernen\n    undo_emoji: '{{ emoji }} Reaktion rückgängig machen'\n    react_emoji: mit {{ emoji }} reagieren\n    unreact_emoji: '{{ emoji }} Reaktion entfernen'\n  comment:\n    btn_add_comment: Einen Kommentar hinzufügen\n    reply_to: Antwort an\n    btn_reply: Antwort\n    btn_edit: Bearbeiten\n    btn_delete: Löschen\n    btn_flag: Melden\n    btn_save_edits: Änderungen speichern\n    btn_cancel: Abbrechen\n    show_more: \"{{count}} mehr Kommentare\"\n    tip_question: >-\n      Verwende Kommentare, um nach weiteren Informationen zu fragen oder Verbesserungen vorzuschlagen. Vermeide es, Fragen in Kommentaren zu beantworten.\n    tip_answer: >-\n      Verwende Stellungsnahmen, um anderen Nutzern zu antworten oder sie über Änderungen zu informieren. Wenn du neue Informationen hinzufügst, bearbeite deinen Beitrag, anstatt zu kommentieren.\n    tip_vote: Es fügt dem Beitrag etwas Nützliches hinzu\n  edit_answer:\n    title: Antwort bearbeiten\n    default_reason: Antwort bearbeiten\n    default_first_reason: Antwort hinzufügen\n    form:\n      fields:\n        revision:\n          label: Version\n        answer:\n          label: Antwort\n          feedback:\n            characters: der Inhalt muss mindestens 6 Zeichen lang sein.\n        edit_summary:\n          label: Zusammenfassung bearbeiten\n          placeholder: >-\n            Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung)\n    btn_save_edits: Änderungen speichern\n    btn_cancel: Abbrechen\n  tags:\n    title: Schlagwörter\n    sort_buttons:\n      popular: Beliebt\n      name: Name\n      newest: Neueste\n    button_follow: Folgen\n    button_following: Folgend\n    tag_label: fragen\n    search_placeholder: Nach Tagnamen filtern\n    no_desc: Der Tag hat keine Beschreibung.\n    more: Mehr\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Frage bearbeiten\n    default_reason: Frage bearbeiten\n    default_first_reason: Create question\n    similar_questions: Ähnliche Fragen\n    form:\n      fields:\n        revision:\n          label: Version\n        title:\n          label: Titel\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Der Titel darf nicht leer sein.\n            range: Titel bis zu 150 Zeichen\n        body:\n          label: Körper\n          msg:\n            empty: Körper darf nicht leer sein.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Stichworte\n          msg:\n            empty: Tags dürfen nicht leer sein.\n        answer:\n          label: Antwort\n          msg:\n            empty: Antwort darf nicht leer sein.\n        edit_summary:\n          label: Zusammenfassung bearbeiten\n          placeholder: >-\n            Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung)\n    btn_post_question: Poste deine Frage\n    btn_save_edits: Änderungen speichern\n    answer_question: Eigene Frage beantworten\n    post_question&answer: Poste deine Frage und Antwort\n  tag_selector:\n    add_btn: Schlagwort hinzufügen\n    create_btn: Neuen Tag erstellen\n    search_tag: Tag suchen\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Keine Tags gefunden\n    tag_required_text: Benötigter Tag (mindestens eins)\n  header:\n    nav:\n      question: Fragen\n      tag: Schlagwörter\n      user: Benutzer\n      badges: Abzeichen\n      profile: Profil\n      setting: Einstellungen\n      logout: Ausloggen\n      admin: Administrator\n      review: Überprüfung\n      bookmark: Lesezeichen\n      moderation: Moderation\n    search:\n      placeholder: Suchen\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Ändern\n    loading: wird geladen...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Gib den Text oben ein\n    msg:\n      empty: Captcha darf nicht leer sein.\n  inactive:\n    first: >-\n      Du bist fast fertig! Wir haben eine Aktivierungsmail an <bold>{{mail}}</bold> geschickt. Bitte folge den Anweisungen in der Mail, um dein Konto zu aktivieren.\n    info: \"Wenn sie nicht ankommt, überprüfe deinen Spam-Ordner.\"\n    another: >-\n      Wir haben dir eine weitere Aktivierungs-E-Mail an <bold>{{mail}}</bold> geschickt. Es kann ein paar Minuten dauern, bis sie ankommt; überprüfe daher deinen Spam-Ordner.\n    btn_name: Aktivierungs Mail erneut senden\n    change_btn_name: E-Mail ändern\n    msg:\n      empty: Kann nicht leer sein.\n    resend_email:\n      url_label: Bist du sicher, dass du die Aktivierungs-E-Mail erneut senden willst?\n      url_text: Du kannst auch den Aktivierungslink oben an den Nutzer weitergeben.\n  login:\n    login_to_continue: Anmelden, um fortzufahren\n    info_sign: Du verfügst noch nicht über ein Konto? Registrieren\n    info_login: Du hast bereits ein Konto? <1>Anmelden</1>\n    agreements: Wenn du dich registrierst, stimmst du der <1>Datenschutzrichtlinie</1> und den <3>Nutzungsbedingungen</3> zu.\n    forgot_pass: Passwort vergessen?\n    name:\n      label: Name\n      msg:\n        empty: Der Name darf nicht leer sein.\n        range: Der Name muss zwischen 2 und 30 Zeichen lang sein.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-Mail\n      msg:\n        empty: E-Mail-Feld darf nicht leer sein.\n    password:\n      label: Passwort\n      msg:\n        empty: Passwort-Feld darf nicht leer sein.\n        different: Die beiden eingegebenen Passwörter stimmen nicht überein\n  account_forgot:\n    page_title: Dein Passwort vergessen\n    btn_name: Schicke mir eine E-Mail zur Wiederherstellung\n    send_success: >-\n      Wenn ein Konto mit <strong>{{mail}}</strong> übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst.\n    email:\n      label: E-Mail\n      msg:\n        empty: E-Mail darf nicht leer sein.\n  change_email:\n    btn_cancel: Stornieren\n    btn_update: E-Mail Adresse aktualisieren\n    send_success: >-\n      Wenn ein Konto mit <strong>{{mail}}</strong> übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst.\n    email:\n      label: Neue E-Mail\n      msg:\n        empty: E-Mail darf nicht leer sein.\n  oauth:\n    connect: Mit {{ auth_name }} verbinden\n    remove: '{{ auth_name }} entfernen'\n  oauth_bind_email:\n    subtitle: Wiederherstellungs-E-Mail zu deinem Konto hinzufügen.\n    btn_update: E-Mail aktualisieren\n    email:\n      label: E-Mail\n      msg:\n        empty: E-Mail darf nicht leer sein.\n    modal_title: E-Mail existiert bereits.\n    modal_content: Diese E-Mail ist bereits registriert. Bist du sicher, dass du dich mit dem bestehenden Konto verbinden möchtest?\n    modal_cancel: E-Mail ändern\n    modal_confirm: Mit dem bestehenden Konto verbinden\n  password_reset:\n    page_title: Passwort zurücksetzen\n    btn_name: Setze mein Passwort zurück\n    reset_success: >-\n      Du hast dein Passwort erfolgreich geändert; du wirst zur Anmeldeseite weitergeleitet.\n    link_invalid: >-\n      Dieser Link zum Zurücksetzen des Passworts ist leider nicht mehr gültig. Vielleicht ist dein Passwort bereits zurückgesetzt?\n    to_login: Weiter zur Anmeldeseite\n    password:\n      label: Passwort\n      msg:\n        empty: Passwort kann nicht leer sein.\n        length: Die Länge muss zwischen 8 und 32 liegen\n        different: Die auf beiden Seiten eingegebenen Passwörter sind inkonsistent\n    password_confirm:\n      label: Neues Passwort bestätigen\n  settings:\n    page_title: Einstellungen\n    goto_modify: Zum Ändern\n    nav:\n      profile: Profil\n      notification: Benachrichtigungen\n      account: Konto\n      interface: Benutzeroberfläche\n    profile:\n      heading: Profil\n      btn_name: Speichern\n      display_name:\n        label: Anzeigename\n        msg: Anzeigename darf nicht leer sein.\n        msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein.\n      username:\n        label: Nutzername\n        caption: Leute können dich als \"@Benutzername\" erwähnen.\n        msg: Benutzername darf nicht leer sein.\n        msg_range: Der Benutzername muss zwischen 2 und 30 Zeichen lang sein.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profilbild\n        gravatar: Gravatar\n        gravatar_text: Du kannst das Bild ändern auf\n        custom: Benutzerdefiniert\n        custom_text: Du kannst dein Bild hochladen.\n        default: System\n        msg: Bitte lade einen Avatar hoch\n      bio:\n        label: Über mich\n      website:\n        label: Webseite\n        placeholder: \"https://example.com\"\n        msg: Website falsches Format\n      location:\n        label: Standort\n        placeholder: \"Stadt, Land\"\n    notification:\n      heading: E-Mail-Benachrichtigungen\n      turn_on: Aktivieren\n      inbox:\n        label: Posteingangsbenachrichtigungen\n        description: Antworten auf deine Fragen, Kommentare, Einladungen und mehr.\n      all_new_question:\n        label: Alle neuen Fragen\n        description: Lass dich über alle neuen Fragen benachrichtigen. Bis zu 50 Fragen pro Woche.\n      all_new_question_for_following_tags:\n        label: Alle neuen Fragen für folgende Tags\n        description: Lass dich über neue Fragen zu folgenden Tags benachrichtigen.\n    account:\n      heading: Konto\n      change_email_btn: E-Mail ändern\n      change_pass_btn: Passwort ändern\n      change_email_info: >-\n        Wir haben eine E-Mail an diese Adresse geschickt. Bitte befolge die Anweisungen zur Bestätigung.\n      email:\n        label: E-Mail\n      new_email:\n        label: Neue E-Mail\n        msg: Neue E-Mail darf nicht leer sein.\n      pass:\n        label: Aktuelles Passwort\n        msg: Passwort kann nicht leer sein.\n      password_title: Passwort\n      current_pass:\n        label: Aktuelles Passwort\n        msg:\n          empty: Das aktuelle Passwort darf nicht leer sein.\n          length: Die Länge muss zwischen 8 und 32 liegen.\n          different: Die beiden eingegebenen Passwörter stimmen nicht überein.\n      new_pass:\n        label: Neues Passwort\n      pass_confirm:\n        label: Neues Passwort bestätigen\n    interface:\n      heading: Benutzeroberfläche\n      lang:\n        label: Sprache der Benutzeroberfläche\n        text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst.\n    my_logins:\n      title: Meine Anmeldungen\n      label: Melde dich mit diesen Konten an oder registriere dich auf dieser Seite.\n      modal_title: Login entfernen\n      modal_content: Bist du sicher, dass du dieses Login aus deinem Konto entfernen möchtest?\n      modal_confirm_btn: Entfernen\n      remove_success: Erfolgreich entfernt\n  toast:\n    update: Aktualisierung erfolgreich\n    update_password: Das Kennwort wurde erfolgreich geändert.\n    flag_success: Danke fürs Markieren.\n    forbidden_operate_self: Verboten, an sich selbst zu operieren\n    review: Deine Überarbeitung wird nach der Überprüfung angezeigt.\n    sent_success: Erfolgreich gesendet\n  related_question:\n    title: Related\n    answers: antworten\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Frage jemanden\n    desc: Lade Leute ein, von denen du glaubst, dass sie die Antwort wissen könnten.\n    invite: Zur Antwort einladen\n    add: Personen hinzufügen\n    search: Personen suchen\n  question_detail:\n    action: Aktion\n    created: Created\n    Asked: Gefragt\n    asked: gefragt\n    update: Geändert\n    Edited: Edited\n    edit: bearbeitet\n    commented: kommentiert\n    Views: Gesehen\n    Follow: Folgen\n    Following: Folgend\n    follow_tip: Folge dieser Frage, um Benachrichtigungen zu erhalten\n    answered: beantwortet\n    closed_in: Abgeschlossen in\n    show_exist: Bestehende Frage anzeigen.\n    useful: Nützlich\n    question_useful: Es ist nützlich und klar\n    question_un_useful: Es ist unklar oder nicht nützlich\n    question_bookmark: Lesezeichen für diese Frage\n    answer_useful: Es ist nützlich\n    answer_un_useful: Es ist nicht nützlich\n    answers:\n      title: Antworten\n      score: Punkte\n      newest: Neueste\n      oldest: Älteste\n      btn_accept: Akzeptieren\n      btn_accepted: Akzeptiert\n    write_answer:\n      title: Deine Antwort\n      edit_answer: Meine existierende Antwort bearbeiten\n      btn_name: Poste deine Antwort\n      add_another_answer: Weitere Antwort hinzufügen\n      confirm_title: Antworten fortsetzen\n      continue: Weitermachen\n      confirm_info: >-\n        <p>Bist du sicher, dass du eine weitere Antwort hinzufügen willst?</p><p>Du könntest stattdessen den Bearbeiten-Link verwenden, um deine existierende Antwort zu verfeinern und zu verbessern.</p>\n      empty: Antwort darf nicht leer sein.\n      characters: der Inhalt muss mindestens 6 Zeichen lang sein.\n      tips:\n        header_1: Danke für deine Antwort\n        li1_1: Bitte stelle sicher, dass du <strong>die Frage beantwortest</strong>. Gib Details an und erzähle von deiner Recherche.\n        li1_2: Untermauere alle Aussagen, die du erstellst, mit Referenzen oder persönlichen Erfahrungen.\n        header_2: Aber <strong>vermeide</strong>...\n        li2_1: Bitte um Hilfe, um Klarstellung oder um Antwort auf andere Antworten.\n    reopen:\n      confirm_btn: Wieder öffnen\n      title: Diesen Beitrag erneut öffnen\n      content: Bist du sicher, dass du wieder öffnen willst?\n    list:\n      confirm_btn: Liste\n      title: Diesen Beitrag auflisten\n      content: Möchten Sie diesen Beitrag wirklich in der Liste anzeigen?\n    unlist:\n      confirm_btn: Von Liste nehmen\n      title: Diesen Beitrag von der Liste nehmen\n      content: Möchten Sie diesen Beitrag wirklich aus der Liste ausblenden?\n    pin:\n      title: Diesen Beitrag anpinnen\n      content: Bist du sicher, dass du den Beitrag global anheften möchtest? Dieser Beitrag wird in allen Beitragslisten ganz oben erscheinen.\n      confirm_btn: Anheften\n  delete:\n    title: Diesen Beitrag löschen\n    question: >-\n      Wir raten davon ab, <strong>Fragen mit Antworten zu löschen</strong>, weil dadurch zukünftigen Lesern dieses Wissen vorenthalten wird.</p><p>Wiederholtes Löschen von beantworteten Fragen kann dazu führen, dass dein Konto für Fragen gesperrt wird. Bist du sicher, dass du löschen möchtest?\n    answer_accepted: >-\n      <p>Wir empfehlen nicht, <strong>akzeptierte Antworten zu löschen</strong>, denn dadurch wird zukünftigen Lesern dieses Wissen vorenthalten. </p> Das wiederholte Löschen von akzeptierten Antworten kann dazu führen, dass dein Konto für die Beantwortung gesperrt wird. Bist du sicher, dass du löschen möchtest?\n    other: Bist du sicher, dass du löschen möchtest?\n    tip_answer_deleted: Diese Antwort wurde gelöscht\n    undelete_title: Diesen Beitrag wiederherstellen\n    undelete_desc: Bist du sicher, dass du die Löschung umkehren willst?\n  btns:\n    confirm: Bestätigen\n    cancel: Abbrechen\n    edit: Bearbeiten\n    save: Speichern\n    delete: Löschen\n    undelete: Wiederherstellen\n    list: Liste\n    unlist: Verstecken\n    unlisted: Versteckt\n    login: Einloggen\n    signup: Registrieren\n    logout: Ausloggen\n    verify: Überprüfen\n    create: Erstellen\n    approve: Genehmigen\n    reject: Ablehnen\n    skip: Überspringen\n    discard_draft: Entwurf verwerfen\n    pinned: Angeheftet\n    all: Alle\n    question: Frage\n    answer: Antwort\n    comment: Kommentar\n    refresh: Aktualisieren\n    resend: Erneut senden\n    deactivate: Deaktivieren\n    active: Aktiv\n    suspend: Sperren\n    unsuspend: Entsperren\n    close: Schließen\n    reopen: Wieder öffnen\n    ok: Okay\n    light: Hell\n    dark: Dunkel\n    system_setting: System-Einstellung\n    default: Standard\n    reset: Zurücksetzen\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignorieren\n    submit: Absenden\n    normal: Normal\n    closed: Geschlossen\n    deleted: Gelöscht\n    deleted_permanently: Dauerhaft gelöscht\n    pending: Ausstehend\n    more: Mehr\n    view: Betrachten\n    card: Karte\n    compact: Kompakt\n    display_below: Unten anzeigen\n    always_display: Immer anzeigen\n    or: oder\n    back_sites: Zurück zur Website\n  search:\n    title: Suchergebnisse\n    keywords: Schlüsselwörter\n    options: Optionen\n    follow: Folgen\n    following: Folgend\n    counts: \"{{count}} Ergebnisse\"\n    counts_loading: \"... Results\"\n    more: Mehr\n    sort_btns:\n      relevance: Relevanz\n      newest: Neueste\n      active: Aktiv\n      score: Punktzahl\n      more: Mehr\n    tips:\n      title: Erweiterte Suchtipps\n      tag: \"<1>[tag]</1> Suche mit einem Tag\"\n      user: \"<1>user:username</1> Suche nach Autor\"\n      answer: \"<1>Antworten:0</1> unbeantwortete Fragen\"\n      score: \"<1>score:3</1> Beiträge mit einer 3+ Punktzahl\"\n      question: \"<1>is:question</1> Suchfragen\"\n      is_answer: \"<1>ist:answer</1> Suchantworten\"\n    empty: Wir konnten nichts finden. <br /> Versuche es mit anderen oder weniger spezifischen Keywords.\n  share:\n    name: Teilen\n    copy: Link kopieren\n    via: Beitrag teilen über...\n    copied: Kopiert\n    facebook: Auf Facebook teilen\n    twitter: Auf X teilen\n  cannot_vote_for_self: Du kannst nicht für deinen eigenen Beitrag stimmen.\n  modal_confirm:\n    title: Fehler...\n  delete_permanently:\n    title: Endgültig löschen\n    content: Sind Sie sicher, dass Sie den Inhalt endgültig löschen möchten?\n  account_result:\n    success: Dein neues Konto ist bestätigt; du wirst zur Startseite weitergeleitet.\n    link: Weiter zur Startseite\n    oops: Hoppla!\n    invalid: Der Link, den Sie verwendet haben, funktioniert nicht mehr.\n    confirm_new_email: Deine E-Mail wurde aktualisiert.\n    confirm_new_email_invalid: >-\n      Dieser Bestätigungslink ist leider nicht mehr gültig. Vielleicht wurde deine E-Mail-Adresse bereits geändert?\n  unsubscribe:\n    page_title: Abonnement entfernen\n    success_title: Erfolgreich vom Abo abgemeldet\n    success_desc: Du wurdest erfolgreich aus der Abonnentenliste gestrichen und wirst keine weiteren E-Mails von uns erhalten.\n    link: Einstellungen ändern\n  question:\n    following_tags: Folgende Tags\n    edit: Bearbeiten\n    save: Speichern\n    follow_tag_tip: Folge den Tags, um deine Liste mit Fragen zu erstellen.\n    hot_questions: Angesagte Fragen\n    all_questions: Alle Fragen\n    x_questions: \"{{ count }} Fragen\"\n    x_answers: \"{{ count }} Antworten\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Fragen\n    answers: Antworten\n    newest: Neueste\n    active: Aktiv\n    hot: Heiß\n    frequent: Häufig\n    recommend: Empfehlen\n    score: Punktzahl\n    unanswered: Unbeantwortet\n    modified: geändert\n    answered: beantwortet\n    asked: gefragt\n    closed: schließen\n    follow_a_tag: Einem Tag folgen\n    more: Mehr\n  personal:\n    overview: Übersicht\n    answers: Antworten\n    answer: antwort\n    questions: Fragen\n    question: frage\n    bookmarks: Lesezeichen\n    reputation: Ansehen\n    comments: Kommentare\n    votes: Stimmen\n    badges: Abzeichen\n    newest: Neueste\n    score: Punktzahl\n    edit_profile: Profil bearbeiten\n    visited_x_days: \"{{ count }} Tage besucht\"\n    viewed: Gesehen\n    joined: Beigetreten\n    comma: \",\"\n    last_login: Gesehen\n    about_me: Über mich\n    about_me_empty: \"// Hallo Welt !\"\n    top_answers: Top-Antworten\n    top_questions: Top-Fragen\n    stats: Statistiken\n    list_empty: Keine Beiträge gefunden.<br />Vielleicht möchtest du einen anderen Reiter auswählen?\n    content_empty: Keine Posts gefunden.\n    accepted: Akzeptiert\n    answered: antwortete\n    asked: gefragt\n    downvoted: negativ bewertet\n    mod_short: MOD\n    mod_long: Moderatoren\n    x_reputation: ansehen\n    x_votes: Stimmen erhalten\n    x_answers: Antworten\n    x_questions: Fragen\n    recent_badges: Neueste Abzeichen\n  install:\n    title: Installation\n    next: Nächste\n    done: Erledigt\n    config_yaml_error: Die Datei config.yaml kann nicht erstellt werden.\n    lang:\n      label: Bitte wähle eine Sprache\n    db_type:\n      label: Datenbank-Engine\n    db_username:\n      label: Nutzername\n      placeholder: wurzel\n      msg: Benutzername darf nicht leer sein.\n    db_password:\n      label: Passwort\n      placeholder: wurzel\n      msg: Passwort kann nicht leer sein.\n    db_host:\n      label: Datenbank-Host\n      placeholder: \"db:3306\"\n      msg: Datenbank-Host darf nicht leer sein.\n    db_name:\n      label: Datenbankname\n      placeholder: antworten\n      msg: Der Datenbankname darf nicht leer sein.\n    db_file:\n      label: Datenbank-Datei\n      placeholder: /data/answer.Weder noch\n      msg: Datenbankdatei kann nicht leer sein.\n    ssl_enabled:\n      label: SSL aktivieren\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL-Modus\n    ssl_root_cert:\n      placeholder: SSL-Root-Zertifikat Pfad\n      msg: Pfad zum Ssl-Root-Zertifikat darf nicht leer sein\n    ssl_cert:\n      placeholder: SSL-Zertifikat Pfad\n      msg: Pfad zum SSL-Zertifikat darf nicht leer sein\n    ssl_key:\n      placeholder: SSL-Key Pfad\n      msg: Der Pfad zum SSL-Key darf nicht leer sein\n    config_yaml:\n      title: config.yaml erstellen\n      label: Die erstellte config.yaml-Datei.\n      desc: >-\n        Du kannst die Datei <1>config.yaml</1> manuell im Verzeichnis <1>/var/wwww/xxx/</1> erstellen und den folgenden Text dort einfügen.\n      info: Nachdem du das getan hast, klickst du auf die Schaltfläche \"Weiter\".\n    site_information: Standortinformationen\n    admin_account: Administratorkonto\n    site_name:\n      label: Seitenname\n      msg: Standortname darf nicht leer sein.\n      msg_max_length: Der Name der Website darf maximal 30 Zeichen lang sein.\n    site_url:\n      label: Seiten-URL\n      text: Die Adresse deiner Website.\n      msg:\n        empty: Die Website-URL darf nicht leer sein.\n        incorrect: Falsches Format der Website-URL.\n        max_length: Die URL der Website darf maximal 512 Zeichen lang sein.\n    contact_email:\n      label: Kontakt E-Mail\n      text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist.\n      msg:\n        empty: Kontakt-E-Mail kann nicht leer sein.\n        incorrect: Falsches Format der Kontakt-E-Mail.\n    login_required:\n      label: Privat\n      switch: Anmeldung erforderlich\n      text: Nur eingeloggte Benutzer können auf diese Community zugreifen.\n    admin_name:\n      label: Name\n      msg: Der Name darf nicht leer sein.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Der Name muss zwischen 2 und 30 Zeichen lang sein.\n    admin_password:\n      label: Passwort\n      text: >-\n        Du brauchst dieses Passwort, um dich einzuloggen. Bitte bewahre es an einem sicheren Ort auf.\n      msg: Passwort kann nicht leer sein.\n      msg_min_length: Passwort muss mindestens 8 Zeichen lang sein.\n      msg_max_length: Das Passwort darf maximal 32 Zeichen lang sein.\n    admin_confirm_password:\n      label: \"Passwort bestätigen\"\n      text: \"Bitte geben Sie Ihr Passwort erneut ein, um es zu bestätigen.\"\n      msg: \"Passwortbestätigung stimmt nicht überein!\"\n    admin_email:\n      label: E-Mail\n      text: Du brauchst diese E-Mail, um dich einzuloggen.\n      msg:\n        empty: E-Mail darf nicht leer sein.\n        incorrect: E-Mail falsches Format.\n    ready_title: Ihre Seite ist bereit\n    ready_desc: >-\n      Wenn du noch mehr Einstellungen ändern möchtest, besuche den <1>Admin-Bereich</1>; du findest ihn im Seitenmenü.\n    good_luck: \"Viel Spaß und viel Glück!\"\n    warn_title: Warnung\n    warn_desc: >-\n      Die Datei <1>config.yaml</1> existiert bereits. Wenn du einen der Konfigurationspunkte in dieser Datei zurücksetzen musst, lösche sie bitte zuerst.\n    install_now: Du kannst versuchen, <1>jetzt zu installieren</1>.\n    installed: Bereits installiert\n    installed_desc: >-\n      Du scheinst es bereits installiert zu haben. Um neu zu installieren, lösche bitte zuerst deine alten Datenbanktabellen.\n    db_failed: Datenbankverbindung fehlgeschlagen\n    db_failed_desc: >-\n      Das bedeutet entweder, dass die Datenbankinformationen in deiner <1>config.yaml</1> Datei falsch sind oder dass der Kontakt zum Datenbankserver nicht hergestellt werden konnte. Das könnte bedeuten, dass der Datenbankserver deines Hosts ausgefallen ist.\n  counts:\n    views: Ansichten\n    votes: Stimmen\n    answers: Antworten\n    accepted: Akzeptiert\n  page_error:\n    http_error: HTTP Fehler {{ code }}\n    desc_403: Du hast keine Berechtigung, auf diese Seite zuzugreifen.\n    desc_404: Leider existiert diese Seite nicht.\n    desc_50X: Der Server ist auf einen Fehler gestoßen und konnte deine Anfrage nicht vollständig abschließen.\n    back_home: Zurück zur Startseite\n  page_maintenance:\n    desc: \"Wir werden gewartet, wir sind bald wieder da.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Inhalt\n    questions: Fragen\n    answers: Antworten\n    users: Benutzer\n    badges: Abzeichen\n    flags: Meldungen\n    settings: Einstellungen\n    general: Allgemein\n    interface: Benutzeroberfläche\n    smtp: SMTP\n    branding: Branding\n    legal: Rechtliches\n    write: Schreiben\n    terms: Terms\n    tos: Nutzungsbedingungen\n    privacy: Privatsphäre\n    seo: SEO\n    customize: Anpassen\n    themes: Themen\n    login: Anmeldung\n    privileges: Berechtigungen\n    plugins: Erweiterungen (Plugins)\n    installed_plugins: Installierte Plugins\n    apperance: Erscheinungsbild\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Willkommen auf {{site_name}}\n  user_center:\n    login: Anmelden\n    qrcode_login_tip: Bitte verwende {{ agentName }}, um den QR-Code zu scannen und dich einzuloggen.\n    login_failed_email_tip: Anmeldung ist fehlgeschlagen. Bitte erlaube dieser App, auf deine E-Mail-Informationen zuzugreifen, bevor du es erneut versuchst.\n  badges:\n    modal:\n      title: Glückwunsch\n      content: Sie haben sich ein neues Abzeichen verdient.\n      close: Schließen\n      confirm: Abzeichen ansehen\n    title: Abzeichen\n    awarded: Verliehen\n    earned_×: Verdiente ×{{ number }}\n    ×_awarded: \"verliehen {{ number }} \"\n    can_earn_multiple: Du kannst das mehrmals verdienen.\n    earned: Verdient\n  admin:\n    admin_header:\n      title: Administrator\n    dashboard:\n      title: Dashboard\n      welcome: Willkommen im Admin Bereich!\n      site_statistics: Website-Statistiken\n      questions: \"Fragen:\"\n      resolved: \"Belöst:\"\n      unanswered: \"Nicht beantwortet:\"\n      answers: \"Antworten:\"\n      comments: \"Kommentare:\"\n      votes: \"Stimmen:\"\n      users: \"Nutzer:\"\n      flags: \"Meldungen:\"\n      reviews: \"Rezension:\"\n      site_health: Gesundheit der Website\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Hochladeverzeichnis:\"\n      run_mode: \"Betriebsmodus:\"\n      private: Privat\n      public: Öffentlich\n      smtp: \"SMTP:\"\n      timezone: \"Zeitzone:\"\n      system_info: Systeminformationen\n      go_version: \"Go Version:\"\n      database: \"Datenbank:\"\n      database_size: \"Datenbankgröße:\"\n      storage_used: \"Verwendeter Speicher:\"\n      uptime: \"Betriebszeit:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Kontakt\n      forum: Forum\n      documents: Dokumentation\n      feedback: Rückmeldung\n      support: Unterstützung\n      review: Überprüfung\n      config: Konfig\n      update_to: Aktualisieren zu\n      latest: Aktuell\n      check_failed: Prüfung fehlgeschlagen\n      \"yes\": \"Ja\"\n      \"no\": \"Nein\"\n      not_allowed: Nicht erlaubt\n      allowed: Erlaubt\n      enabled: Aktiviert\n      disabled: Deaktiviert\n      writable: Schreibbar\n      not_writable: Nicht schreibbar\n    flags:\n      title: Meldungen\n      pending: Ausstehend\n      completed: Abgeschlossen\n      flagged: Gekennzeichnet\n      flagged_type: '{{ type }} gemeldet'\n      created: Erstellt\n      action: Aktion\n      review: Überprüfung\n    user_role_modal:\n      title: Benutzerrolle ändern zu...\n      btn_cancel: Abbrechen\n      btn_submit: Senden\n    new_password_modal:\n      title: Neues Passwort festlegen\n      form:\n        fields:\n          password:\n            label: Passwort\n            text: Der Nutzer wird abgemeldet und muss sich erneut anmelden.\n            msg: Das Passwort muss mindestens 8-32 Zeichen lang sein.\n      btn_cancel: Abbrechen\n      btn_submit: Senden\n    edit_profile_modal:\n      title: Profil bearbeiten\n      form:\n        fields:\n          display_name:\n            label: Anzeigename\n            msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein.\n          username:\n            label: Nutzername\n            msg_range: Der Benutzername muss 2-30 Zeichen lang sein.\n          email:\n            label: E-Mail\n            msg_invalid: Ungültige E-Mail-Adresse.\n      edit_success: Erfolgreich bearbeitet\n      btn_cancel: Abbrechen\n      btn_submit: Absenden\n    user_modal:\n      title: Neuen Benutzer hinzufügen\n      form:\n        fields:\n          users:\n            label: Masse Benutzer hinzufügen\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Trenne \"Name, E-Mail, Passwort\" mit Kommas. Ein Benutzer pro Zeile.\n            msg: \"Bitte gib die E-Mail des Nutzers ein, eine pro Zeile.\"\n          display_name:\n            label: Anzeigename\n            msg: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein.\n          email:\n            label: E-Mail\n            msg: Die E-Mail ist nicht gültig.\n          password:\n            label: Passwort\n            msg: Das Passwort muss mindestens 8-32 Zeichen lang sein.\n      btn_cancel: Abbrechen\n      btn_submit: Senden\n    users:\n      title: Benutzer\n      name: Name\n      email: E-Mail\n      reputation: Ansehen\n      created_at: Angelegt am\n      delete_at: Löschzeit\n      suspend_at: Sperrzeit\n      suspend_until: Suspend until\n      status: Status\n      role: Rolle\n      action: Aktion\n      change: Ändern\n      all: Alle\n      staff: Teammitglieder\n      more: Mehr\n      inactive: Inaktiv\n      suspended: Gesperrt\n      deleted: Gelöscht\n      normal: Normal\n      Moderator: Moderation\n      Admin: Administrator\n      User: Benutzer\n      filter:\n        placeholder: \"Nach Namen, user:id filtern\"\n      set_new_password: Neues Passwort festlegen\n      edit_profile: Profil bearbeiten\n      change_status: Status ändern\n      change_role: Rolle wechseln\n      show_logs: Protokolle anzeigen\n      add_user: Benutzer hinzufügen\n      deactivate_user:\n        title: Benutzer deaktivieren\n        content: Ein inaktiver Nutzer muss seine E-Mail erneut bestätigen.\n      delete_user:\n        title: Diesen Benutzer löschen\n        content: Bist du sicher, dass du diesen Benutzer löschen willst? Das ist dauerhaft!\n        remove: Ihren Inhalt entfernen\n        label: Alle Fragen, Antworten, Kommentare, etc. entfernen\n        text: Aktiviere diese Option nicht, wenn du nur das Benutzerkonto löschen möchtest.\n      suspend_user:\n        title: Diesen Benutzer sperren\n        content: Ein gesperrter Benutzer kann sich nicht einloggen.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Fragen\n      unlisted: Nicht gelistet\n      post: Beitrag\n      votes: Stimmen\n      answers: Antworten\n      created: Erstellt\n      status: Status\n      action: Aktion\n      change: Ändern\n      pending: Ausstehend\n      filter:\n        placeholder: \"Filtern nach Titel, Frage:Id\"\n    answers:\n      page_title: Antworten\n      post: Beitrag\n      votes: Stimmen\n      created: Erstellt\n      status: Status\n      action: Aktion\n      change: Ändern\n      filter:\n        placeholder: \"Filtern nach Titel, Antwort: id\"\n    general:\n      page_title: Allgemein\n      name:\n        label: Seitenname\n        msg: Der Site-Name darf nicht leer sein.\n        text: \"Der Name dieser Website, wie er im Titel-Tag verwendet wird.\"\n      site_url:\n        label: Seiten-URL\n        msg: Die Website-Url darf nicht leer sein.\n        validate: Bitte gib eine gültige URL ein.\n        text: Die Adresse deiner Website.\n      short_desc:\n        label: Kurze Seitenbeschreibung\n        msg: Die kurze Website-Beschreibung darf nicht leer sein.\n        text: \"Kurze Beschreibung, wie im Titel-Tag auf der Homepage verwendet.\"\n      desc:\n        label: Seitenbeschreibung\n        msg: Die Websitebeschreibung darf nicht leer sein.\n        text: \"Beschreibe diese Seite in einem Satz, wie er im Meta Description Tag verwendet wird.\"\n      contact_email:\n        label: Kontakt E-Mail\n        msg: Kontakt-E-Mail darf nicht leer sein.\n        validate: Kontakt-E-Mail ist ungültig.\n        text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist.\n      check_update:\n        label: Softwareaktualisierungen\n        text: Automatisch auf Updates prüfen\n    interface:\n      page_title: Benutzeroberfläche\n      language:\n        label: Interface Sprache\n        msg: Sprache der Benutzeroberfläche darf nicht leer sein.\n        text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst.\n      time_zone:\n        label: Zeitzone\n        msg: Die Zeitzone darf nicht leer sein.\n        text: Wähle eine Stadt in der gleichen Zeitzone wie du.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Von E-Mail\n        msg: Von E-Mail darf nicht leer sein.\n        text: Die E-Mail-Adresse, von der E-Mails gesendet werden.\n      from_name:\n        label: Von Name\n        msg: Absendername darf nicht leer sein.\n        text: Der Name, von dem E-Mails gesendet werden.\n      smtp_host:\n        label: SMTP-Host\n        msg: Der SMTP-Host darf nicht leer sein.\n        text: Dein Mailserver.\n      encryption:\n        label: Verschlüsselung\n        msg: Verschlüsselung darf nicht leer sein.\n        text: Für die meisten Server ist SSL die empfohlene Option.\n        ssl: SSL\n        tls: TLS\n        none: Keine\n      smtp_port:\n        label: SMTP-Port\n        msg: SMTP-Port muss Nummer 1 ~ 65535 sein.\n        text: Der Port zu deinem Mailserver.\n      smtp_username:\n        label: SMTP-Benutzername\n        msg: Der SMTP-Benutzername darf nicht leer sein.\n      smtp_password:\n        label: SMTP-Kennwort\n        msg: Das SMTP-Passwort darf nicht leer sein.\n      test_email_recipient:\n        label: Test-E-Mail-Empfänger\n        text: Gib die E-Mail-Adresse an, an die Testsendungen gesendet werden sollen.\n        msg: Test-E-Mail-Empfänger ist ungültig\n      smtp_authentication:\n        label: Authentifizierung aktivieren\n        title: SMTP-Authentifizierung\n        msg: Die SMTP-Authentifizierung darf nicht leer sein.\n        \"yes\": \"Ja\"\n        \"no\": \"Nein\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo darf nicht leer sein.\n        text: Das Logobild oben links auf deiner Website. Verwende ein breites rechteckiges Bild mit einer Höhe von 56 und einem Seitenverhältnis von mehr als 3:1. Wenn du es leer lässt, wird der Text des Website-Titels angezeigt.\n      mobile_logo:\n        label: Mobiles Logo\n        text: Das Logo wird auf der mobilen Version deiner Website verwendet. Verwende ein breites rechteckiges Bild mit einer Höhe von 56. Wenn du nichts angibst, wird das Bild aus der Einstellung \"Logo\" verwendet.\n      square_icon:\n        label: Quadratisches Symbol\n        msg: Quadratisches Symbol darf nicht leer sein.\n        text: Bild, das als Basis für Metadatensymbole verwendet wird. Sollte idealerweise größer als 512x512 sein.\n      favicon:\n        label: Favicon\n        text: Ein Favicon für deine Website. Um korrekt über ein CDN zu funktionieren, muss es ein png sein. Es wird auf 32x32 verkleinert. Wenn du es leer lässt, wird das \"quadratische Symbol\" verwendet.\n    legal:\n      page_title: Rechtliches\n      terms_of_service:\n        label: Nutzungsbedingungen\n        text: \"Du kannst hier Inhalte zu den Nutzungsbedingungen hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an.\"\n      privacy_policy:\n        label: Datenschutzbestimmungen\n        text: \"Du kannst hier Inhalte zur Datenschutzerklärung hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an.\"\n      external_content_display:\n        label: Externer Inhalt\n        text: \"Inhalte umfassen Bilder, Videos und Medien, die von externen Websites eingebettet sind.\"\n        always_display: Externen Inhalt immer anzeigen\n        ask_before_display: Vor der Anzeige externer Inhalte fragen\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Antwort bearbeiten\n        label: Jeder Benutzer kann für jede Frage nur eine Antwort schreiben\n        text: \"Schalten Sie aus, um es Benutzern zu ermöglichen, mehrere Antworten auf dieselbe Frage zu schreiben, was dazu führen kann, dass Antworten nicht im Fokus stehen.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Empfohlene Tags\n        text: \"Empfohlene Tags werden standardmäßig in der Dropdown-Liste angezeigt.\"\n        msg:\n          contain_reserved: \"empfohlene Tags dürfen keine reservierten Tags enthalten\"\n      required_tag:\n        title: Benötigte Tags festlegen\n        label: '\"Empfohlene Tags\" als erforderliche Tags festlegen'\n        text: \"Jede neue Frage muss mindestens ein Empfehlungs-Tag haben.\"\n      reserved_tags:\n        label: Reservierte Tags\n        text: \"Reservierte Tags können nur vom Moderator verwendet werden.\"\n      image_size:\n        label: Maximale Bildgröße (MB)\n        text: \"Die maximale Bildladegröße.\"\n      attachment_size:\n        label: Maximale Anhanggröße (MB)\n        text: \"Die maximale Dateigröße für Dateianhänge.\"\n      image_megapixels:\n        label: Max. BildmePixel\n        text: \"Maximale Anzahl an Megapixeln für ein Bild.\"\n      image_extensions:\n        label: Autorisierte Bilderweiterungen\n        text: \"Eine Liste von Dateierweiterungen, die für die Anzeige von Bildern erlaubt sind, getrennt durch Kommata.\"\n      attachment_extensions:\n        label: Autorisierte Anhänge Erweiterungen\n        text: \"Eine Liste von Dateierweiterungen, die für das Hochladen erlaubt sind, getrennt mit Kommas. WARNUNG: Erlaubt Uploads kann Sicherheitsprobleme verursachen.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Dauerlink\n        text: Benutzerdefinierte URL-Strukturen können die Benutzerfreundlichkeit und die Vorwärtskompatibilität deiner Links verbessern.\n      robots:\n        label: robots.txt\n        text: Dadurch werden alle zugehörigen Site-Einstellungen dauerhaft überschrieben.\n    themes:\n      page_title: Themen\n      themes:\n        label: Themen\n        text: Wähle ein bestehendes Thema aus.\n      color_scheme:\n        label: Farbschema\n      navbar_style:\n        label: Hintergrundstil der Navigationsleiste\n      primary_color:\n        label: Primäre Farbe\n        text: Ändere die Farben, die von deinen Themes verwendet werden\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS und HTML\n      custom_css:\n        label: Benutzerdefinierte CSS\n        text: >\n\n      head:\n        label: Kopf\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Fusszeile\n        text: Dies wird vor </body> eingefügt.\n      sidebar:\n        label: Seitenleiste\n        text: Dies wird in die Seitenleiste eingefügt.\n    login:\n      page_title: Anmeldung\n      membership:\n        title: Mitgliedschaft\n        label: Neuregistrierungen zulassen\n        text: Schalte sie ab, um zu verhindern, dass jemand ein neues Konto erstellt.\n      email_registration:\n        title: E-Mail Registrierung\n        label: E-Mail-Registrierung zulassen\n        text: Abschalten, um zu verhindern, dass jemand ein neues Konto per E-Mail erstellt.\n      allowed_email_domains:\n        title: Zugelassene E-Mail-Domänen\n        text: E-Mail-Domänen, bei denen die Nutzer Konten registrieren müssen. Eine Domäne pro Zeile. Wird ignoriert, wenn leer.\n      private:\n        title: Privatgelände\n        label: Anmeldung erforderlich\n        text: Nur angemeldete Benutzer können auf diese Community zugreifen.\n      password_login:\n        title: Passwort-Login\n        label: E-Mail-und Passwort-Login erlauben\n        text: \"WARNUNG: Wenn du diese Option abschaltest, kannst du dich möglicherweise nicht mehr anmelden, wenn du zuvor keine andere Anmeldemethode konfiguriert hast.\"\n    installed_plugins:\n      title: Installierte Plugins\n      plugin_link: Plugins erweitern und ergänzen die Funktionalität. Du kannst Plugins im <1>Pluginverzeichnis</1> finden.\n      filter:\n        all: Alle\n        active: Aktiv\n        inactive: Inaktiv\n        outdated: Veraltet\n      plugins:\n        label: Erweiterungen\n        text: Wähle ein bestehendes Plugin aus.\n      name: Name\n      version: Version\n      status: Status\n      action: Aktion\n      deactivate: Deaktivieren\n      activate: Aktivieren\n      settings: Einstellungen\n    settings_users:\n      title: Benutzer\n      avatar:\n        label: Standard-Avatar\n        text: Für Benutzer ohne einen eigenen Avatar.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL der API-Basis des Gravatar-Anbieters. Wird ignoriert, wenn leer.\n      profile_editable:\n        title: Profil bearbeitbar\n      allow_update_display_name:\n        label: Benutzern erlauben, ihren Anzeigenamen zu ändern\n      allow_update_username:\n        label: Benutzern erlauben, ihren Benutzernamen zu ändern\n      allow_update_avatar:\n        label: Benutzern erlauben, ihr Profilbild zu ändern\n      allow_update_bio:\n        label: Benutzern erlauben, ihr Über mich zu ändern\n      allow_update_website:\n        label: Benutzern erlauben, ihre Website zu ändern\n      allow_update_location:\n        label: Benutzern erlauben, ihren Standort zu ändern\n    privilege:\n      title: Berechtigungen\n      level:\n        label: Benötigtes Reputations-Level\n        text: Wähle die für die Privilegien erforderliche Reputation aus\n      msg:\n        should_be_number: die Eingabe muss numerisch sein\n        number_larger_1: Zahl muss gleich oder größer als 1 sein\n    badges:\n      action: Aktion\n      active: Aktiv\n      activate: Aktivieren\n      all: Alle\n      awards: Verliehen\n      deactivate: Deaktivieren\n      filter:\n        placeholder: Nach Namen, Abzeichen:id filtern\n      group: Gruppe\n      inactive: Inaktiv\n      name: Name\n      show_logs: Protokolle anzeigen\n      status: Status\n      title: Abzeichen\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: kann nicht leer sein\n    invalid: ist ungültig\n    btn_submit: Speichern\n    not_found_props: \"Erforderliche Eigenschaft {{ key }} nicht gefunden.\"\n    select: Auswählen\n  page_review:\n    review: Überprüfung\n    proposed: vorgeschlagen\n    question_edit: Frage bearbeiten\n    answer_edit: Antwort bearbeiten\n    tag_edit: Tag bearbeiten\n    edit_summary: Zusammenfassung bearbeiten\n    edit_question: Frage bearbeiten\n    edit_answer: Antwort bearbeiten\n    edit_tag: Tag bearbeiten\n    empty: Keine Überprüfungsaufgaben mehr übrig.\n    approve_revision_tip: Akzeptieren Sie diese Revision?\n    approve_flag_tip: Sind Sie mit diesem Bericht einverstanden?\n    approve_post_tip: Bestätigen Sie diesen Beitrag?\n    approve_user_tip: Bestätigen Sie diesen Benutzer?\n    suggest_edits: Änderungsvorschläge\n    flag_post: Beitrag melden\n    flag_user: Nutzer melden\n    queued_post: Beitrag in Warteschlange\n    queued_user: Benutzer in der Warteschlange\n    filter_label: Typ\n    reputation: ansehen\n    flag_post_type: Diesen Beitrag als {{ type }} markiert.\n    flag_user_type: Diesen Benutzer als {{ type }} markiert.\n    edit_post: Beitrag bearbeiten\n    list_post: Ausgestellte Beiträge\n    unlist_post: Versteckte Beiträge\n  timeline:\n    undeleted: ungelöscht\n    deleted: gelöscht\n    downvote: ablehnen\n    upvote: positiv bewerten\n    accept: akzeptieren\n    cancelled: abgebrochen\n    commented: kommentiert\n    rollback: zurückrollen\n    edited: bearbeitet\n    answered: antwortete\n    asked: gefragt\n    closed: geschlossen\n    reopened: wiedereröffnet\n    created: erstellt\n    pin: angeheftet\n    unpin: losgelöst\n    show: gelistet\n    hide: nicht gelistet\n    title: \"Verlauf von\"\n    tag_title: \"Zeitleiste für\"\n    show_votes: \"Stimmen anzeigen\"\n    n_or_a: Keine Angaben\n    title_for_question: \"Zeitleiste für\"\n    title_for_answer: \"Zeitachse für die Antwort auf {{ title }} von {{ author }}\"\n    title_for_tag: \"Zeitachse für Tag\"\n    datetime: Terminzeit\n    type: Typ\n    by: Von\n    comment: Kommentar\n    no_data: \"Wir konnten nichts finden.\"\n  users:\n    title: Benutzer\n    users_with_the_most_reputation: Benutzer mit den höchsten Reputationspunkten dieser Woche\n    users_with_the_most_vote: Benutzer, die diese Woche am meisten gestimmt haben\n    staffs: Unsere Community Teammitglieder\n    reputation: Ansehen\n    votes: Stimmen\n  prompt:\n    leave_page: Bist du sicher, dass du die Seite verlassen willst?\n    changes_not_save: Deine Änderungen werden möglicherweise nicht gespeichert.\n  draft:\n    discard_confirm: Bist du sicher, dass du deinen Entwurf verwerfen willst?\n  messages:\n    post_deleted: Dieser Beitrag wurde gelöscht.\n    post_cancel_deleted: Dieser Beitrag wurde wiederhergestellt.\n    post_pin: Dieser Beitrag wurde angepinnt.\n    post_unpin: Dieser Beitrag wurde losgelöst.\n    post_hide_list: Dieser Beitrag wurde aus der Liste verborgen.\n    post_show_list: Dieser Beitrag wird in der Liste angezeigt.\n    post_reopen: Dieser Beitrag wurde wieder geöffnet.\n    post_list: Dieser Beitrag wurde angezeigt.\n    post_unlist: Dieser Beitrag wurde ausgeblendet.\n    post_pending: Dein Beitrag wartet auf eine Überprüfung. Dies ist eine Vorschau, sie wird nach der Genehmigung sichtbar sein.\n    post_closed: Dieser Beitrag wurde gelöscht.\n    answer_deleted: Diese Antwort wurde gelöscht.\n    answer_cancel_deleted: Diese Antwort wurde wiederhergestellt.\n    change_user_role: Die Rolle dieses Benutzers wurde geändert.\n    user_inactive: Dieser Benutzer ist bereits inaktiv.\n    user_normal: Dieser Benutzer ist bereits normal.\n    user_suspended: Dieser Nutzer wurde gesperrt.\n    user_deleted: Benutzer wurde gelöscht.\n    user_added: User has been added successfully.\n    badge_activated: Dieses Abzeichen wurde aktiviert.\n    badge_inactivated: Dieses Abzeichen wurde deaktiviert.\n    users_deleted: Der Benutzer wurde gelöscht.\n    posts_deleted: Deine Frage wurde gelöscht.\n    answers_deleted: Deine Antwort wurde gelöscht.\n    copy: In die Zwischenablage kopieren\n    copied: Kopiert\n    external_content_warning: Externe Bilder/Medien werden nicht angezeigt.\n\n\n"
  },
  {
    "path": "i18n/el_GR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/en_US.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\n\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Edit\n    delete:\n      other: Delete\n    close:\n      other: Close\n    reopen:\n      other: Reopen\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: List\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Password\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: You cannot modify your password.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li>\n      <li><p class=\"mb-2\">to make links</p><pre\n      class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p\n      class=\"mb-2\">put returns between paragraphs</p></li><li><p\n      class=\"mb-2\"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p\n      class=\"mb-2\">indent code by 4 spaces</p></li><li><p class=\"mb-2\">quote by\n      placing <code>&gt;</code> at start of line</p></li><li><p\n      class=\"mb-2\">backtick escapes <code>`like _this_`</code></p></li><li><p\n      class=\"mb-2\">create code fences with backticks <code>`</code></p><pre\n      class=\"mb-0\"><code>```<br/>code here<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Edit Tag\n    ask_a_question: Create Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    new_alerts: New alerts\n    all_read: Mark all as read\n    show_more: Show more\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar,\n            improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p>\n        <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p>\n        <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid\n      answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are\n      adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar,\n            improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar,\n            improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>.\n      Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might\n      take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email\n      with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email\n      with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in\n      page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is\n      already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation\n        instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Invite People\n    desc: Invite people you think can answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the\n        edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      We do not recommend <strong>deleting questions with answers</strong> because\n      doing so deprives future readers of this knowledge.</p><p>Repeated deletion\n      of answered questions can result in your account being blocked from asking.\n      Are you sure you wish to delete?\n    answer_accepted: >-\n      <p>We do not recommend <strong>deleting accepted answer</strong> because\n      doing so deprives future readers of this knowledge. </p> Repeated deletion\n      of accepted answers can result in your account being blocked from answering.\n      Are you sure you wish to delete?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was\n      already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        You can create the <1>config.yaml</1> file manually in the\n        <1>/var/wwww/xxx/</1> directory and paste the following text into it.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure\n        location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>;\n      find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the\n      configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old\n      database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for the same question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as &lt;link>\n      head:\n        label: Head\n        text: This will insert before &lt;/head>\n      header:\n        label: Header\n        text: This will insert after &lt;body>\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/es_ES.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Completado.\n    unknown:\n      other: Error desconocido.\n    request_format_error:\n      other: Formato de la solicitud inválido.\n    unauthorized_error:\n      other: No autorizado.\n    database_error:\n      other: Error en el servidor de datos.\n    forbidden_error:\n      other: Prohibido.\n    duplicate_request_error:\n      other: Solicitud duplicada.\n  action:\n    report:\n      other: Reportar\n    edit:\n      other: Editar\n    delete:\n      other: Eliminar\n    close:\n      other: Cerrar\n    reopen:\n      other: Reabrir\n    forbidden_error:\n      other: Prohibido.\n    pin:\n      other: Fijar\n    hide:\n      other: Retirar\n    unpin:\n      other: Desfijar\n    show:\n      other: Lista\n    invite_someone_to_answer:\n      other: Editar\n    undelete:\n      other: Recuperar\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Usuario\n      admin:\n        other: Administrador\n      moderator:\n        other: Moderador\n    description:\n      user:\n        other: Predeterminado sin acceso especial.\n      admin:\n        other: Dispone de acceso total al sitio web y sus ajustes.\n      moderator:\n        other: Dispone de acceso a todas las publicaciones pero no a los ajustes de administrador.\n  privilege:\n    level_1:\n      description:\n        other: Nivel 1 (reputación menor requerida para un equipo privado, grupo)\n    level_2:\n      description:\n        other: Nivel 2 (reputación baja requerida para una comunidad de Startup)\n    level_3:\n      description:\n        other: Nivel 3 (reputación alta requerida para una comunidad madura)\n    level_custom:\n      description:\n        other: Nivel personalizado\n    rank_question_add_label:\n      other: Hacer una pregunta\n    rank_answer_add_label:\n      other: Escribir respuesta\n    rank_comment_add_label:\n      other: Escribir comentario\n    rank_report_add_label:\n      other: Reportar\n    rank_comment_vote_up_label:\n      other: Votar comentario a favor\n    rank_link_url_limit_label:\n      other: Publica más de 2 enlaces a la vez\n    rank_question_vote_up_label:\n      other: Votar pregunta a favor\n    rank_answer_vote_up_label:\n      other: Votar respuesta a favor\n    rank_question_vote_down_label:\n      other: Votar pregunta en contra\n    rank_answer_vote_down_label:\n      other: Votar respuesta en contra\n    rank_invite_someone_to_answer_label:\n      other: Invitar a alguien a responder\n    rank_tag_add_label:\n      other: Crear nueva etiqueta\n    rank_tag_edit_label:\n      other: Editar descripción de etiqueta (revisión necesaria)\n    rank_question_edit_label:\n      other: Editar pregunta ajena (revisión necesaria)\n    rank_answer_edit_label:\n      other: Editar respuesta ajena (revisión necesaria)\n    rank_question_edit_without_review_label:\n      other: Editar pregunta ajena sin revisión\n    rank_answer_edit_without_review_label:\n      other: Editar respuesta ajena sin revisión\n    rank_question_audit_label:\n      other: Revisar ediciones de pregunta\n    rank_answer_audit_label:\n      other: Revisar ediciones de respuesta\n    rank_tag_audit_label:\n      other: Revisar ediciones de etiqueta\n    rank_tag_edit_without_review_label:\n      other: Editar descripción de etiqueta sin revisión\n    rank_tag_synonym_label:\n      other: Administrar sinónimos de etiqueta\n  email:\n    other: Correo electrónico\n  e_mail:\n    other: Correo electrónico\n  password:\n    other: Contraseña\n  pass:\n    other: Contraseña\n  old_pass:\n    other: Current password\n  original_text:\n    other: Esta publicación\n  email_or_password_wrong_error:\n    other: Contraseña o correo incorrecto.\n  error:\n    common:\n      invalid_url:\n        other: URL no válido.\n      status_invalid:\n        other: Estado inválido.\n    password:\n      space_invalid:\n        other: La contraseña no puede contener espacios.\n    admin:\n      cannot_update_their_password:\n        other: No puede modificar su contraseña.\n      cannot_edit_their_profile:\n        other: No puede modificar su perfil.\n      cannot_modify_self_status:\n        other: No puede modificar su contraseña.\n      email_or_password_wrong:\n        other: Contraseña o correo incorrecto.\n    answer:\n      not_found:\n        other: Respuesta no encontrada.\n      cannot_deleted:\n        other: Sin permiso para eliminar.\n      cannot_update:\n        other: Sin permiso para actualizar.\n      question_closed_cannot_add:\n        other: Las preguntas están cerradas y no pueden añadirse.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Edición del comentario no permitida.\n      not_found:\n        other: Comentario no encontrado.\n      cannot_edit_after_deadline:\n        other: El tiempo del comentario ha sido demasiado largo para modificarlo.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Correo electrónico ya en uso.\n      need_to_be_verified:\n        other: El correo debe ser verificado.\n      verify_url_expired:\n        other: La URL verificada del correo electrónico ha caducado. Por favor, vuelva a enviar el correo electrónico.\n      illegal_email_domain_error:\n        other: No está permitido el correo electrónico de ese dominio. Por favor utilice otro.\n    lang:\n      not_found:\n        other: Archivo de idioma no encontrado.\n    object:\n      captcha_verification_failed:\n        other: Captcha fallido.\n      disallow_follow:\n        other: No dispones de permiso para seguir.\n      disallow_vote:\n        other: No dispones de permiso para votar.\n      disallow_vote_your_self:\n        other: No puedes votar a tu propia publicación.\n      not_found:\n        other: Objeto no encontrado.\n      verification_failed:\n        other: Verificación fallida.\n      email_or_password_incorrect:\n        other: Contraseña o correo incorrecto.\n      old_password_verification_failed:\n        other: La verificación de la contraseña antigua falló\n      new_password_same_as_previous_setting:\n        other: La nueva contraseña es igual a la anterior.\n      already_deleted:\n        other: Esta publicación ha sido borrada.\n    meta:\n      object_not_found:\n        other: Meta objeto no encontrado\n    question:\n      already_deleted:\n        other: Esta publicación ha sido eliminada.\n      under_review:\n        other: Tu publicación está siendo revisada. Será visible una vez sea aprobada.\n      not_found:\n        other: Pregunta no encontrada.\n      cannot_deleted:\n        other: Sin permiso para eliminar.\n      cannot_close:\n        other: Sin permiso para cerrar.\n      cannot_update:\n        other: Sin permiso para actualizar.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: El rango de reputación no cumple la condición.\n      vote_fail_to_meet_the_condition:\n        other: Gracias por los comentarios. Necesitas al menos reputación {{.Rank}} para votar.\n      no_enough_rank_to_operate:\n        other: Necesitas al menos reputación {{.Rank}} para hacer esto.\n    report:\n      handle_failed:\n        other: Error en el manejador del reporte.\n      not_found:\n        other: Informe no encontrado.\n    tag:\n      already_exist:\n        other: La etiqueta ya existe.\n      not_found:\n        other: Etiqueta no encontrada.\n      recommend_tag_not_found:\n        other: La etiqueta recomendada no existe.\n      recommend_tag_enter:\n        other: Por favor, introduce al menos una de las etiquetas requeridas.\n      not_contain_synonym_tags:\n        other: No debe contener etiquetas sinónimas.\n      cannot_update:\n        other: Sin permiso para actualizar.\n      is_used_cannot_delete:\n        other: No puedes eliminar una etiqueta que está en uso.\n      cannot_set_synonym_as_itself:\n        other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: El nombre no puede ser una dirección de correo electrónico.\n    theme:\n      not_found:\n        other: Tema no encontrado.\n    revision:\n      review_underway:\n        other: No se puede editar actualmente, hay una versión en la cola de revisiones.\n      no_permission:\n        other: Sin permisos para ver.\n    user:\n      external_login_missing_user_id:\n        other: La plataforma de terceros no proporciona un UserID único, por lo que si no puede iniciar sesión, contacte al administrador del sitio.\n      external_login_unbinding_forbidden:\n        other: Por favor añada una contraseña de inicio de sesión a su cuenta antes de eliminar este método de acceso.\n      email_or_password_wrong:\n        other:\n          other: Contraseña o correo incorrecto.\n      not_found:\n        other: Usuario no encontrado.\n      suspended:\n        other: El usuario ha sido suspendido.\n      username_invalid:\n        other: Nombre de usuario no válido.\n      username_duplicate:\n        other: El nombre de usuario ya está en uso.\n      set_avatar:\n        other: Fallo al actualizar el avatar.\n      cannot_update_your_role:\n        other: No puedes modificar tu propio rol.\n      not_allowed_registration:\n        other: Actualmente el sitio no está abierto para el registro.\n      not_allowed_login_via_password:\n        other: Actualmente el sitio no está abierto para iniciar sesión por contraseña.\n      access_denied:\n        other: Acceso denegado\n      page_access_denied:\n        other: No tienes acceso a esta página.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} formato cerca de '{{.Content}}' en la línea {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"El número de usuarios que añadas a la vez debe estar en el rango de 1 a {{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Lectura de configuración fallida\n    database:\n      connection_failed:\n        other: Conexión a la base de datos fallida\n      create_table_failed:\n        other: Creación de tabla fallida\n    install:\n      create_config_failed:\n        other: No es posible crear el archivo config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Formato de archivo no soportado.\n    site_info:\n      config_not_found:\n        other: Configuración del sitio no encontrada.\n    badge:\n      object_not_found:\n        other: Insignia no encontrada\n  reason:\n    spam:\n      name:\n        other: correo no deseado\n      desc:\n        other: Esta publicación es un anuncio, o vandalismo. No es útil o relevante para el tema actual.\n    rude_or_abusive:\n      name:\n        other: grosero u ofensivo\n      desc:\n        other: \"Alguna persona podría considerar este contenido inapropiado para una discusión respetuosa.\"\n    a_duplicate:\n      name:\n        other: un duplicado\n      desc:\n        other: Esta pregunta ha sido hecha antes y ya ha sido resuelta.\n      placeholder:\n        other: Introduce el enlace de la pregunta existente\n    not_a_answer:\n      name:\n        other: no es una respuesta\n      desc:\n        other: \"Esto fue publicado como respuesta pero no intenta responder a la pregunta. Podría ser una edición, un comentario, otra pregunta diferente o ser eliminado por completo.\"\n    no_longer_needed:\n      name:\n        other: ya no es necesario\n      desc:\n        other: Este comentario está desactualizado, es conversacional o no es relevante a esta publicación.\n    something:\n      name:\n        other: otro motivo\n      desc:\n        other: Esta publicación requiere revisión del personal por otro motivo no listado arriba.\n      placeholder:\n        other: Háganos saber qué le interesa en concreto\n    community_specific:\n      name:\n        other: un motivo determinado de la comunidad\n      desc:\n        other: Esta pregunta no cumple con una norma comunitaria.\n    not_clarity:\n      name:\n        other: requiere detalles o aclaraciones\n      desc:\n        other: Esta pregunta actualmente incluye múltiples preguntas en una. Debería enfocarse en una única cuestión.\n    looks_ok:\n      name:\n        other: parece correcto\n      desc:\n        other: Esta publicación es buena como es y no es de baja calidad.\n    needs_edit:\n      name:\n        other: necesita editarse, y lo hice\n      desc:\n        other: Mejora y corrige los problemas con esta publicación personalmente.\n    needs_close:\n      name:\n        other: necesita cerrar\n      desc:\n        other: Una pregunta cerrada no puede responderse, pero aún se puede editar, votar y comentar.\n    needs_delete:\n      name:\n        other: necesita eliminación\n      desc:\n        other: Esta publicación será eliminada.\n  question:\n    close:\n      duplicate:\n        name:\n          other: correo no deseado\n        desc:\n          other: La pregunta ya ha sido preguntada y resuelta previamente.\n      guideline:\n        name:\n          other: razón específica de la comunidad\n        desc:\n          other: Esta pregunta infringe alguna norma de la comunidad.\n      multiple:\n        name:\n          other: necesita más detalles o aclaraciónes\n        desc:\n          other: Esta pregunta incluye múltiples preguntas en una sola. Debería centrarse únicamente en un solo tema.\n      other:\n        name:\n          other: otra razón\n        desc:\n          other: Esta publicación requiere otra razón no listada arriba.\n    operation_type:\n      asked:\n        other: preguntada\n      answered:\n        other: respondida\n      modified:\n        other: modificada\n    deleted_title:\n      other: Pregunta eliminada\n    questions_title:\n      other: Preguntas\n  tag:\n    tags_title:\n      other: Etiquetas\n    no_description:\n      other: La etiqueta no tiene descripción.\n  notification:\n    action:\n      update_question:\n        other: pregunta actualizada\n      answer_the_question:\n        other: pregunta respondidas\n      update_answer:\n        other: respuesta actualizada\n      accept_answer:\n        other: respuesta aceptada\n      comment_question:\n        other: pregunta comentada\n      comment_answer:\n        other: respuesta comentada\n      reply_to_you:\n        other: te ha respondido\n      mention_you:\n        other: te ha mencionado\n      your_question_is_closed:\n        other: Tu pregunta ha sido cerrada\n      your_question_was_deleted:\n        other: Tu pregunta ha sido eliminada\n      your_answer_was_deleted:\n        other: Tu respuesta ha sido eliminada\n      your_comment_was_deleted:\n        other: Tu comentario ha sido eliminado\n      up_voted_question:\n        other: pregunta votada a favor\n      down_voted_question:\n        other: pregunta votada en contra\n      up_voted_answer:\n        other: respuesta votada a favor\n      down_voted_answer:\n        other: respuesta votada en contra\n      up_voted_comment:\n        other: comentario votado a favor\n      invited_you_to_answer:\n        other: te invitó a responder\n      earned_badge:\n        other: Ha ganado la insignia \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirma tu nueva dirección de correo\"\n      body:\n        other: \"Confirme su nueva dirección de correo electrónico para {{.SiteName}} haciendo clic en el siguiente enlace:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nSi no solicitó este cambio, por favor, ignore este mensaje.<br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} respondió tu pregunta\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Verlo en {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Darse de baja</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} te invitó a responder\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Es posible que conozca la respuesta.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Verlo en {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Darse de baja</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} comentó en tu publicación\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Verlo en {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Darse de baja</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nueva pregunta: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Reestablecimiento de contraseña\"\n      body:\n        other: \"Se solicitó el restablecimiento de su contraseña en {{.SiteName}}.<br><br>\\n\\nSi no lo solicitó, puede ignorar este mensaje.<br><br>\\n\\nHaga clic en el siguiente enlace para generar una nueva contraseña:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirma tu nueva cuenta\"\n      body:\n        other: \"¡Bienvenido a {{.SiteName}}!<br><br>\\n\\nHaga clic en el siguiente enlace y active su nueva cuenta:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nSi no puede hacer clic en el enlace anterior, intente copiando y pegando el enlace en la barra de dirección en su navegador web.<br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Correo de prueba\"\n      body:\n        other: \"Este es un mensaje de prueba.\\n<br><br>\\n\\n--<br>\\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.\"\n  action_activity_type:\n    upvote:\n      other: votar a favor\n    upvoted:\n      other: votado a favor\n    downvote:\n      other: voto negativo\n    downvoted:\n      other: votado en contra\n    accept:\n      other: aceptar\n    accepted:\n      other: aceptado\n    edit:\n      other: editar\n  review:\n    queued_post:\n      other: Publicación en cola\n    flagged_post:\n      other: Publicación marcada\n    suggested_post_edit:\n      other: Ediciones sugeridas\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} y {{ .Count }} más...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiógrafo\n        desc:\n          other: Completó la información de su <a href=\"{{ .ProfileURL }}\" target=\"_blank\">perfil</a>.\n      certified:\n        name:\n          other: Certificado\n        desc:\n          other: Completó nuestro nuevo tutorial de usuario.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: Primer mensaje editado.\n      first_flag:\n        name:\n          other: Primera Denuncia\n        desc:\n          other: Primer denuncia de un post.\n      first_upvote:\n        name:\n          other: Primer voto favorable\n        desc:\n          other: Primera vez que le doy un 'like' a un post.\n      first_link:\n        name:\n          other: Primer Enlace\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: Primera reacción\n        desc:\n          other: Primero reaccionó al post.\n      first_share:\n        name:\n          other: Primer Compartir\n        desc:\n          other: Primero compartió un post.\n      scholar:\n        name:\n          other: Erudito\n        desc:\n          other: Hecha una pregunta y aceptada una respuesta.\n      commentator:\n        name:\n          other: Comentador\n        desc:\n          other: Deja 5 comentarios.\n      new_user_of_the_month:\n        name:\n          other: Nuevo usuario del mes\n        desc:\n          other: Contribuciones pendientes en su primer mes.\n      read_guidelines:\n        name:\n          other: Lea los lineamientos\n        desc:\n          other: Lea las [directrices de la comunidad].\n      reader:\n        name:\n          other: Lector\n        desc:\n          other: Lee cada respuesta en un tema con más de 10 respuestas.\n      welcome:\n        name:\n          other: Bienvenido\n        desc:\n          other: Recibió un voto a favor.\n      nice_share:\n        name:\n          other: Buena Compartición\n        desc:\n          other: Compartió un post con 25 visitantes únicos.\n      good_share:\n        name:\n          other: Buena Compartida\n        desc:\n          other: Compartió un post con 300 visitantes únicos.\n      great_share:\n        name:\n          other: Excelente Compartida\n        desc:\n          other: Compartió un post con 1000 visitantes únicos.\n      out_of_love:\n        name:\n          other: Fuera del Amor\n        desc:\n          other: Utilizó 50 votos positivos en un día.\n      higher_love:\n        name:\n          other: Amor Más Alto\n        desc:\n          other: Utilizó 50 votos positivos en un día 5 veces.\n      crazy_in_love:\n        name:\n          other: Loco(a) por el Amor\n        desc:\n          other: Utilizó 50 votos positivos en un día 20 veces.\n      promoter:\n        name:\n          other: Promotor\n        desc:\n          other: Invitó a un usuario.\n      campaigner:\n        name:\n          other: Activista\n        desc:\n          other: Invitó a 3 usuarios básicos.\n      champion:\n        name:\n          other: Campeón\n        desc:\n          other: Invitado 5 miembros.\n      thank_you:\n        name:\n          other: Gracias\n        desc:\n          other: Tiene 20 publicaciones con votos positivos y dio 10 votos positivos.\n      gives_back:\n        name:\n          other: Da a Cambio\n        desc:\n          other: Tiene 100 publicaciones con votos positivos y dio 100 votos positivos.\n      empathetic:\n        name:\n          other: Empático\n        desc:\n          other: Tiene 500 publicaciones con votos positivos y dio 1000 votos positivos.\n      enthusiast:\n        name:\n          other: Entusiasta\n        desc:\n          other: Visita 10 días consecutivos.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visita 100 días consecutivos.\n      devotee:\n        name:\n          other: Devoto\n        desc:\n          other: Visita 365 días consecutivos.\n      anniversary:\n        name:\n          other: Aniversario\n        desc:\n          other: Miembro activo por un año, publicó al menos una vez.\n      appreciated:\n        name:\n          other: Apreciación\n        desc:\n          other: Recibió 1 voto positivo en 20 puestos.\n      respected:\n        name:\n          other: Respetado\n        desc:\n          other: Recibió 2 voto positivo en 100 puestos.\n      admired:\n        name:\n          other: Admirado\n        desc:\n          other: Recibió 5 voto positivo en 300 puestos.\n      solved:\n        name:\n          other: Resuelto\n        desc:\n          other: Tener una respuesta aceptada.\n      guidance_counsellor:\n        name:\n          other: Consejero de Orientación\n        desc:\n          other: Tener 10 respuestas aceptadas.\n      know_it_all:\n        name:\n          other: Sabelotodo\n        desc:\n          other: Tener 50 respuestas aceptadas.\n      solution_institution:\n        name:\n          other: Institución de Soluciones\n        desc:\n          other: Tener 150 respuestas aceptadas.\n      nice_answer:\n        name:\n          other: Buena Respuesta\n        desc:\n          other: Respuesta con una puntuación de 10 o más.\n      good_answer:\n        name:\n          other: Excelente Respuesta\n        desc:\n          other: Respuesta con una puntuación de 25 o más.\n      great_answer:\n        name:\n          other: Gran Respuesta\n        desc:\n          other: Respuesta con una puntuación de 50 o más.\n      nice_question:\n        name:\n          other: Buena Pregunta\n        desc:\n          other: Pregunta con una puntuación de 10 o más.\n      good_question:\n        name:\n          other: Excelente Pregunta\n        desc:\n          other: Pregunta con una puntuación de 25 o más.\n      great_question:\n        name:\n          other: Gran Pregunta\n        desc:\n          other: Pregunta con una puntuación de 50 o más.\n      popular_question:\n        name:\n          other: Pregunta popular\n        desc:\n          other: Pregunta con 500 puntos de vista.\n      notable_question:\n        name:\n          other: Pregunta Notable\n        desc:\n          other: Pregunta con 1,000 vistas.\n      famous_question:\n        name:\n          other: Pregunta Famosa\n        desc:\n          other: Pregunta con 5,000 vistas.\n      popular_link:\n        name:\n          other: Enlace Popular\n        desc:\n          other: Publicado un enlace externo con 50 clics.\n      hot_link:\n        name:\n          other: Enlace caliente\n        desc:\n          other: Publicado un enlace externo con 300 clics.\n      famous_link:\n        name:\n          other: Enlace familiar\n        desc:\n          other: Publicado un enlace externo con 100 clics.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Primeros pasos\n      community:\n        name:\n          other: Comunidad\n      posting:\n        name:\n          other: Publicación\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Cómo formatear\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">menciona una publicación: <code>#post_id</code></p></li> <li><p class =\"mb-2\">para hacer enlaces</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Título](https://url.com)</code></pre></li><li><p class=\"mb-2\">poner retornos entre párrafos</p></li><li><p class=\"mb-2\"><em>_italic_</em> o **<strong>negrita</ strong>**</p></li><li><p class=\"mb-2\">sangría del código con 4 espacios</p></li><li><p class=\"mb-2\" >cita colocando <code>&gt;</code> al inicio de la línea</p></li><li><p class=\"mb-2\">comillas invertidas se escapa <code>`like _this_`</code></p></li><li><p class=\"mb-2\">crear barreras de código con comillas invertidas <code> `</code></p><pre class=\"mb-0\"><code>```<br/>código aquí<br/>```</code></pre></li> </ul>\n  pagination:\n    prev: Anterior\n    next: Siguiente\n  page_title:\n    question: Pregunta\n    questions: Preguntas\n    tag: Etiqueta\n    tags: Etiquetas\n    tag_wiki: wiki de Etiquetas\n    create_tag: Crear etiqueta\n    edit_tag: Editar etiqueta\n    ask_a_question: Create Question\n    edit_question: Editar Pregunta\n    edit_answer: Editar respuesta\n    search: Buscar\n    posts_containing: Publicaciones que contienen\n    settings: Ajustes\n    notifications: Notificaciones\n    login: Acceder\n    sign_up: Registrarse\n    account_recovery: Recuperación de la cuenta\n    account_activation: Activación de la cuenta\n    confirm_email: Confirmar correo electrónico\n    account_suspended: Cuenta suspendida\n    admin: Administrador\n    change_email: Modificar correo\n    install: Instalación de Answer\n    upgrade: Actualización de Answer\n    maintenance: Mantenimiento del sitio web\n    users: Usuarios\n    oauth_callback: Procesando\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Cerrar sesión\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notificaciones\n    inbox: Buzón de entrada\n    achievement: Logros\n    new_alerts: Nuevas alertas\n    all_read: Marcar todo como leído\n    show_more: Mostrar más\n    someone: Alguien\n    inbox_type:\n      all: Todo\n      posts: Publicaciones\n      invites: Invitaciones\n      votes: Votos\n    answer: Respuesta\n    question: Pregunta\n    badge_award: Medalla\n  suspended:\n    title: Tu cuenta ha sido suspendida\n    until_time: \"Tu cuenta ha sido suspendida hasta el {{ time }}.\"\n    forever: Este usuario ha sido suspendido indefinidamente.\n    end: Has infringido alguna norma de la comunidad.\n    contact_us: Contáctanos\n  editor:\n    blockquote:\n      text: Cita\n    bold:\n      text: Negrita\n    chart:\n      text: Gráfica\n      flow_chart: Diagrama de flujo\n      sequence_diagram: Diagrama de secuencia\n      class_diagram: Diagrama de clase\n      state_diagram: Diagrama de estado\n      entity_relationship_diagram: Diagrama de relación de entidad\n      user_defined_diagram: Diagrama definido por el usuario\n      gantt_chart: Diagrama de Gantt\n      pie_chart: Grafico de torta\n    code:\n      text: Código\n      add_code: Añadir código\n      form:\n        fields:\n          code:\n            label: Código\n            msg:\n              empty: Código no puede estar vacío.\n          language:\n            label: Idioma\n            placeholder: Detección automática\n      btn_cancel: Cancelar\n      btn_confirm: Añadir\n    formula:\n      text: Fórmula\n      options:\n        inline: Fórmula en línea\n        block: Bloque de fórmula\n    heading:\n      text: Encabezado\n      options:\n        h1: Encabezado 1\n        h2: Encabezado 2\n        h3: Encabezado 3\n        h4: Encabezado 4\n        h5: Encabezado 5\n        h6: Encabezado 6\n    help:\n      text: Ayuda\n    hr:\n      text: Regla horizontal\n    image:\n      text: Imagen\n      add_image: Añadir imagen\n      tab_image: Subir imagen\n      form_image:\n        fields:\n          file:\n            label: Archivo de imagen\n            btn: Seleccionar imagen\n            msg:\n              empty: El título no puede estar vacío.\n              only_image: Solo se permiten archivos de imagen.\n              max_size: El tamaño del archivo no puede exceder {{size}} MB.\n          desc:\n            label: Descripción\n      tab_url: URL de la imagen\n      form_url:\n        fields:\n          url:\n            label: URL de la imagen\n            msg:\n              empty: La URL de la imagen no puede estar vacía.\n          name:\n            label: Descripción\n      btn_cancel: Cancelar\n      btn_confirm: Añadir\n      uploading: Subiendo\n    indent:\n      text: Sangría\n    outdent:\n      text: Quitar sangría\n    italic:\n      text: Cursiva\n    link:\n      text: Enlace\n      add_link: Añadir enlace\n      form:\n        fields:\n          url:\n            label: Por sus siglas en ingles (Localizador Uniforme de recursos), dirección electrónica de un sitio web\n            msg:\n              empty: La dirección no puede estar vacía.\n          name:\n            label: Descripción\n      btn_cancel: Cancelar\n      btn_confirm: Añadir\n    ordered_list:\n      text: Lista numerada\n    unordered_list:\n      text: Lista con viñetas\n    table:\n      text: Tabla\n      heading: Encabezado\n      cell: Celda\n    file:\n      text: Adjuntar archivos\n      not_supported: \"No soporta ese tipo de archivo. Inténtalo de nuevo con {{file_type}}.\"\n      max_size: \"El tamaño de los archivos adjuntos no puede exceder {{size}} MB.\"\n  close_modal:\n    title: Estoy cerrando este post como...\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: No puede estar en blanco.\n    msg:\n      empty: Por favor selecciona una razón.\n  report_modal:\n    flag_title: Estoy marcando para reportar este post de...\n    close_title: Estoy cerrando este post como...\n    review_question_title: Revisar pregunta\n    review_answer_title: Revisar respuesta\n    review_comment_title: Revisar comentario\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: No puede estar en blanco.\n    msg:\n      empty: Por favor selecciona una razón.\n      not_a_url: El formato de la URL es incorrecto.\n      url_not_match: El origen de la URL no coincide con el sitio web actual.\n  tag_modal:\n    title: Crear nueva etiqueta\n    form:\n      fields:\n        display_name:\n          label: Nombre público\n          msg:\n            empty: El nombre a mostrar no puede estar vacío.\n            range: Nombre a mostrar con un máximo de 35 caracteres.\n        slug_name:\n          label: Ruta de la URL\n          desc: Slug de URL de hasta 35 caracteres.\n          msg:\n            empty: URL no puede estar vacío.\n            range: URL slug hasta 35 caracteres.\n            character: La URL amigable contiene caracteres no permitidos.\n        desc:\n          label: Descripción\n        revision:\n          label: Revisión\n        edit_summary:\n          label: Editar resumen\n          placeholder: >-\n            Explica brevemente los cambios (corrección ortográfica, mejora de formato)\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    btn_post: Publicar nueva etiqueta\n  tag_info:\n    created_at: Creado\n    edited_at: Editado\n    history: Historial\n    synonyms:\n      title: Sinónimos\n      text: Las siguientes etiquetas serán reasignadas a\n      empty: No se encontraron sinónimos.\n      btn_add: Añadir un sinónimo\n      btn_edit: Editar\n      btn_save: Guardar\n    synonyms_text: Las siguientes etiquetas serán reasignadas a\n    delete:\n      title: Eliminar esta etiqueta\n      tip_with_posts: >-\n        <p>No permitimos <strong>eliminar etiquetas con publicaciones</strong>.</p> <p>Primero elimine esta etiqueta de las publicaciones.</p>\n      tip_with_synonyms: >-\n        <p>No permitimos <strong>eliminar etiqueta con sinónimos</strong>.</p> <p>Primero elimine los sinónimos de esta etiqueta.</p>\n      tip: '¿Estás seguro de que deseas borrarlo?'\n      close: Cerrar\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Editar etiqueta\n    default_reason: Editar etiqueta\n    default_first_reason: Añadir etiqueta\n    btn_save_edits: Guardar cambios\n    btn_cancel: Cancelar\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [a las] HH:mm\"\n    now: ahora\n    x_seconds_ago: \"hace {{count}}s\"\n    x_minutes_ago: \"hace {{count}}m\"\n    x_hours_ago: \"hace {{count}}h\"\n    hour: hora\n    day: día\n    hours: horas\n    days: días\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: corazón\n    smile: sonrisa\n    frown: frown\n    btn_label: añadir o eliminar reacciones\n    undo_emoji: deshacer reacción de {{ emoji }}\n    react_emoji: reaccionar con {{ emoji }}\n    unreact_emoji: desreaccionar con {{ emoji }}\n  comment:\n    btn_add_comment: Añadir comentario\n    reply_to: Responder a\n    btn_reply: Responder\n    btn_edit: Editar\n    btn_delete: Eliminar\n    btn_flag: Reportar\n    btn_save_edits: Guardar cambios\n    btn_cancel: Cancelar\n    show_more: \"{{count}} comentarios más\"\n    tip_question: >-\n      Utiliza los comentarios para pedir más información o sugerir mejoras y modificaciones. Evita responder preguntas en los comentarios.\n    tip_answer: >-\n      Usa comentarios para responder a otros usuarios o notificarles de cambios. Si estás añadiendo nueva información, edita tu publicación en vez de comentar.\n    tip_vote: Añade algo útil a la publicación\n  edit_answer:\n    title: Editar respuesta\n    default_reason: Editar respuesta\n    default_first_reason: Añadir respuesta\n    form:\n      fields:\n        revision:\n          label: Revisión\n        answer:\n          label: Respuesta\n          feedback:\n            characters: El contenido debe tener al menos 6 caracteres.\n        edit_summary:\n          label: Editar resumen\n          placeholder: >-\n            Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado)\n    btn_save_edits: Guardar cambios\n    btn_cancel: Cancelar\n  tags:\n    title: Etiquetas\n    sort_buttons:\n      popular: Popular\n      name: Nombre\n      newest: Más reciente\n    button_follow: Seguir\n    button_following: Siguiendo\n    tag_label: preguntas\n    search_placeholder: Filtrar por nombre de etiqueta\n    no_desc: La etiqueta no tiene descripción.\n    more: Mas\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Editar pregunta\n    default_reason: Editar pregunta\n    default_first_reason: Create question\n    similar_questions: Preguntas similares\n    form:\n      fields:\n        revision:\n          label: Revisión\n        title:\n          label: Título\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: El título no puede estar vacío.\n            range: Título hasta 150 caracteres\n        body:\n          label: Cuerpo\n          msg:\n            empty: Cuerpo no puede estar vacío.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Etiquetas\n          msg:\n            empty: Se requiere al menos una etiqueta.\n        answer:\n          label: Respuesta\n          msg:\n            empty: La respuesta no puede estar vacía.\n        edit_summary:\n          label: Editar resumen\n          placeholder: >-\n            Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado)\n    btn_post_question: Publica tu pregunta\n    btn_save_edits: Guardar cambios\n    answer_question: Responde a tu propia pregunta\n    post_question&answer: Publicar una pregunta y su respuesta\n  tag_selector:\n    add_btn: Añadir etiqueta\n    create_btn: Crear nueva etiqueta\n    search_tag: Buscar etiqueta\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Ninguna etiqueta coincide\n    tag_required_text: Etiqueta requerida (al menos una)\n  header:\n    nav:\n      question: Preguntas\n      tag: Etiquetas\n      user: Usuarios\n      badges: Insignias\n      profile: Perfil\n      setting: Ajustes\n      logout: Cerrar sesión\n      admin: Administrador\n      review: Revisar\n      bookmark: Marcadores\n      moderation: Moderación\n    search:\n      placeholder: Buscar\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Cambiar\n    loading: cargando...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Introduce el texto anterior\n    msg:\n      empty: El Captcha no puede estar vacío.\n  inactive:\n    first: >-\n      ¡Casi estás listo! Te hemos enviado un correo de activación a <bold>{{mail}}</bold>. Por favor, sigue las instrucciones en el correo para activar tu cuenta.\n    info: \"Si no te ha llegado el correo, comprueba la carpeta de SPAM.\"\n    another: >-\n      Te hemos enviado otro correo de activación a <bold>{{mail}}</bold>. Puede tardar algunos minutos en llegar; asegúrate de revisar tu carpeta de SPAM.\n    btn_name: Reenviar correo de activación\n    change_btn_name: Cambiar correo\n    msg:\n      empty: No puede estar en blanco.\n    resend_email:\n      url_label: '¿Estás seguro de reenviar el correo de activación?'\n      url_text: También puedes dar el enlace de activación de arriba al usuario.\n  login:\n    login_to_continue: Inicia sesión para continuar\n    info_sign: '¿No tienes cuenta? <1>Regístrate</1>'\n    info_login: '¿Ya tienes una cuenta? <1>Inicia sesión</1>'\n    agreements: Al registrarte, aceptas la <1>política de privacidad</1> y los <3>términos de servicio</3>.\n    forgot_pass: '¿Has olvidado la contraseña?'\n    name:\n      label: Nombre\n      msg:\n        empty: El nombre no puede estar vacío.\n        range: El nombre debe tener entre 2 y 30 caracteres de largo.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Correo electrónico\n      msg:\n        empty: El correo electrónico no puede estar vacío.\n    password:\n      label: Contraseña\n      msg:\n        empty: La contraseña no puede estar vacía.\n        different: Las contraseñas introducidas en ambos lados no coinciden\n  account_forgot:\n    page_title: Olvidaste Tu Contraseña\n    btn_name: Enviadme un correo electrónico de recuperación\n    send_success: >-\n      Si existe una cuenta con el correo <strong>{{mail}}</strong>, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente.\n    email:\n      label: Correo electrónico\n      msg:\n        empty: El correo electrónico no puede estar vacío.\n  change_email:\n    btn_cancel: Cancelar\n    btn_update: Actualizar dirección de correo\n    send_success: >-\n      Si existe una cuenta con el correo <strong>{{mail}}</strong>, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente.\n    email:\n      label: Nuevo correo\n      msg:\n        empty: El correo electrónico no puede estar vacío.\n  oauth:\n    connect: Conectar con {{ auth_name }}\n    remove: Eliminar {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Añade un correo de recuperación a tu cuenta.\n    btn_update: Actualizar dirección de correo\n    email:\n      label: Correo\n      msg:\n        empty: El correo no puede estar vacío.\n    modal_title: El correo ya está en uso.\n    modal_content: Este correo electrónico ha sido registrado. ¿Estás seguro de conectarlo a la cuenta existente?\n    modal_cancel: Cambiar correo\n    modal_confirm: Conectarse a la cuenta existente\n  password_reset:\n    page_title: Restablecimiento de Contraseña\n    btn_name: Restablecer mi contraseña\n    reset_success: >-\n      Tu contraseña ha sido actualizada con éxito; vas a ser redirigido a la página de inicio de sesión.\n    link_invalid: >-\n      Lo sentimos, este enlace de restablecimiento de contraseña ya no es válido. ¿Tal vez tu contraseña ya está restablecida?\n    to_login: Continuar a la página de inicio de sesión\n    password:\n      label: Contraseña\n      msg:\n        empty: La contraseña no puede estar vacía.\n        length: La longitud debe ser de entre 8 y 32 caracteres\n        different: Las contraseñas introducidas en ambos lados no coinciden\n    password_confirm:\n      label: Confirmar nueva contraseña\n  settings:\n    page_title: Ajustes\n    goto_modify: Ir a modificar\n    nav:\n      profile: Perfil\n      notification: Notificaciones\n      account: Cuenta\n      interface: Interfaz\n    profile:\n      heading: Perfil\n      btn_name: Guardar\n      display_name:\n        label: Nombre público\n        msg: El nombre a mostrar no puede estar vacío.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Nombre de usuario\n        caption: La gente puede mencionarte con \"@nombredeusuario\".\n        msg: El nombre de usuario no puede estar vacío.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Imagen de perfil\n        gravatar: Gravatar\n        gravatar_text: Puedes cambiar la imagen en\n        custom: Propia\n        custom_text: Puedes subir tu propia imagen.\n        default: Sistema\n        msg: Por favor, sube una imagen\n      bio:\n        label: Sobre mí\n      website:\n        label: Sitio Web\n        placeholder: \"https://example.com\"\n        msg: Formato del sitio web incorrecto\n      location:\n        label: Ubicación\n        placeholder: \"Ciudad, País\"\n    notification:\n      heading: Notificaciones por correo\n      turn_on: Activar\n      inbox:\n        label: Notificaciones de bandeja\n        description: Respuestas a tus preguntas, comentarios, invitaciones, y más.\n      all_new_question:\n        label: Todas las preguntas nuevas\n        description: Recibe notificaciones de todas las preguntas nuevas. Hasta 50 preguntas por semana.\n      all_new_question_for_following_tags:\n        label: Todas las preguntas nuevas para las etiquetas siguientes\n        description: Recibe notificaciones de nuevas preguntas para las etiquetas siguientes.\n    account:\n      heading: Cuenta\n      change_email_btn: Cambiar correo electrónico\n      change_pass_btn: Cambiar contraseña\n      change_email_info: >-\n        Te hemos enviado un email a esa dirección. Por favor sigue las instrucciones de confirmación.\n      email:\n        label: Correo\n      new_email:\n        label: Nuevo correo\n        msg: El nuevo correo no puede estar vacío.\n      pass:\n        label: Contraseña actual\n        msg: La contraseña no puede estar vacía.\n      password_title: Contraseña\n      current_pass:\n        label: Contraseña actual\n        msg:\n          empty: La contraseña actual no puede estar vacía.\n          length: El largo necesita estar entre 8 y 32 caracteres.\n          different: Las contraseñas no coinciden.\n      new_pass:\n        label: Nueva contraseña\n      pass_confirm:\n        label: Confirmar nueva contraseña\n    interface:\n      heading: Interfaz\n      lang:\n        label: Idioma de Interfaz\n        text: Idioma de la interfaz de usuario. Cambiará cuando actualices la página.\n    my_logins:\n      title: Mis accesos\n      label: Inicia sesión o regístrate en este sitio usando estas cuentas.\n      modal_title: Eliminar acceso\n      modal_content: '¿Estás seguro de querer eliminar esta sesión de tu cuenta?'\n      modal_confirm_btn: Eliminar\n      remove_success: Eliminado con éxito\n  toast:\n    update: actualización correcta\n    update_password: Contraseña cambiada con éxito.\n    flag_success: Gracias por reportar.\n    forbidden_operate_self: No puedes modificar tu propio usuario\n    review: Tu revisión será visible luego de ser aprobada.\n    sent_success: Enviado con éxito\n  related_question:\n    title: Related\n    answers: respuestas\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Personas Preguntadas\n    desc: Selecciona personas que creas que sepan la respuesta.\n    invite: Invitar a responder\n    add: Añadir personas\n    search: Buscar personas\n  question_detail:\n    action: Acción\n    created: Created\n    Asked: Preguntada\n    asked: preguntada\n    update: Modificada\n    Edited: Edited\n    edit: editada\n    commented: comentado\n    Views: Visto\n    Follow: Seguir\n    Following: Siguiendo\n    follow_tip: Sigue esta pregunta para recibir notificaciones\n    answered: respondida\n    closed_in: Cerrado el\n    show_exist: Mostrar una pregunta existente.\n    useful: Útil\n    question_useful: Es útil y claro\n    question_un_useful: Es poco claro o no es útil\n    question_bookmark: Añadir esta pregunta a marcadores\n    answer_useful: Es útil\n    answer_un_useful: No es útil\n    answers:\n      title: Respuestas\n      score: Puntuación\n      newest: Más reciente\n      oldest: Más antiguo\n      btn_accept: Aceptar\n      btn_accepted: Aceptada\n    write_answer:\n      title: Tu respuesta\n      edit_answer: Editar mi respuesta existente\n      btn_name: Publica tu respuesta\n      add_another_answer: Añadir otra respuesta\n      confirm_title: Continuar a pregunta\n      continue: Continuar\n      confirm_info: >-\n        <p>¿Seguro que quieres añadir otra respuesta?</p><p>Puedes utilizar el enlace de edición para detallar y mejorar tu respuesta existente en su lugar.</p>\n      empty: La respuesta no puede estar vacía.\n      characters: el contenido debe tener al menos 6 caracteres.\n      tips:\n        header_1: Gracias por tu respuesta\n        li1_1: Asegúrate de <strong>responder la pregunta</strong>. Proporciona detalles y comparte tu investigación.\n        li1_2: Respalda cualquier declaración que hagas con referencias o experiencia personal.\n        header_2: Pero <strong>evita</strong> ...\n        li2_1: Pedir ayuda, pedir aclaraciones, o responder a otras respuestas.\n    reopen:\n      confirm_btn: Reabrir\n      title: Reabrir esta publicación\n      content: '¿Seguro que quieres reabrir esta publicación?'\n    list:\n      confirm_btn: Lista\n      title: Listar esta publicación\n      content: '¿Estás seguro de que quieres listar?'\n    unlist:\n      confirm_btn: Deslistar\n      title: No listar esta publicación\n      content: '¿Estás seguro de que quieres dejar de listar?'\n    pin:\n      title: Fijar esta publicación\n      content: '¿Estás seguro de querer fijar esto globalmente? Esta publicación aparecerá por encima de todas las listas de publicaciones.'\n      confirm_btn: Fijar\n  delete:\n    title: Eliminar esta publicación\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: '¿Estás seguro de que deseas borrarlo?'\n    tip_answer_deleted: Esta respuesta ha sido eliminada\n    undelete_title: Restaurar esta publicación\n    undelete_desc: '¿Estás seguro de querer restaurar?'\n  btns:\n    confirm: Confirmar\n    cancel: Cancelar\n    edit: Editar\n    save: Guardar\n    delete: Eliminar\n    undelete: Restaurar\n    list: Lista\n    unlist: Deslistar\n    unlisted: Sin enumerar\n    login: Acceder\n    signup: Registrarse\n    logout: Cerrar sesión\n    verify: Verificar\n    create: Create\n    approve: Aprobar\n    reject: Rechazar\n    skip: Omitir\n    discard_draft: Descartar borrador\n    pinned: Fijado\n    all: Todo\n    question: Pregunta\n    answer: Respuesta\n    comment: Comentario\n    refresh: Actualizar\n    resend: Reenviar\n    deactivate: Desactivar\n    active: Activar\n    suspend: Suspender\n    unsuspend: Quitar suspensión\n    close: Cerrar\n    reopen: Reabrir\n    ok: Aceptar\n    light: Claro\n    dark: Oscuro\n    system_setting: Ajuste de sistema\n    default: Por defecto\n    reset: Reiniciar\n    tag: Etiqueta\n    post_lowercase: publicación\n    filter: Filtro\n    ignore: Ignorar\n    submit: Enviar\n    normal: Normal\n    closed: Cerrado\n    deleted: Eliminado\n    deleted_permanently: Deleted permanently\n    pending: Pendiente\n    more: Más\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Resultados de la búsqueda\n    keywords: Palabras claves\n    options: Opciones\n    follow: Seguir\n    following: Siguiendo\n    counts: \"{{count}} Resultados\"\n    counts_loading: \"... Results\"\n    more: Más\n    sort_btns:\n      relevance: Relevancia\n      newest: Más reciente\n      active: Activas\n      score: Puntuación\n      more: Mas\n    tips:\n      title: Consejos de búsqueda avanzada\n      tag: \"<1>[tag]</1> búsqueda por etiquetas\"\n      user: \"<1>user:username</1> búsqueda por autor\"\n      answer: \"<1>answers:0</1> preguntas sin responder\"\n      score: \"<1>score:3</1> Publicaciones con un puntaje de 3 o más\"\n      question: \"<1>is:question</1> buscar preguntas\"\n      is_answer: \"<1>is:answer</1> buscar respuestas\"\n    empty: No pudimos encontrar nada. <br /> Prueba a buscar con palabras diferentes o menos específicas.\n  share:\n    name: Compartir\n    copy: Copiar enlace\n    via: Compartir vía...\n    copied: Copiado\n    facebook: Compartir en Facebook\n    twitter: Share to X\n  cannot_vote_for_self: No puedes votar tu propia publicación.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Tu nueva cuenta ha sido confirmada, serás redirigido a la página de inicio.\n    link: Continuar a la página de inicio\n    oops: '¡Ups!'\n    invalid: El enlace que utilizaste ya no funciona.\n    confirm_new_email: Tu email ha sido actualizado.\n    confirm_new_email_invalid: >-\n      Lo siento, este enlace de confirmación ya no es válido. ¿Quizás ya se haya cambiado tu correo electrónico?\n  unsubscribe:\n    page_title: Desuscribir\n    success_title: Desuscrito con éxito\n    success_desc: Ha sido eliminado con éxito de esta lista de suscriptores y no recibirá más correos electrónicos nuestros.\n    link: Cambiar ajustes\n  question:\n    following_tags: Etiquetas seguidas\n    edit: Editar\n    save: Guardar\n    follow_tag_tip: Sigue etiquetas para personalizar tu lista de preguntas.\n    hot_questions: Preguntas del momento\n    all_questions: Todas las preguntas\n    x_questions: \"{{ count }} Preguntas\"\n    x_answers: \"{{ count }} respuestas\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Preguntas\n    answers: Respuestas\n    newest: Más reciente\n    active: Activo\n    hot: Popular\n    frequent: Frecuente\n    recommend: Recomendar\n    score: Puntuación\n    unanswered: Sin respuesta\n    modified: modificada\n    answered: respondida\n    asked: preguntada\n    closed: cerrada\n    follow_a_tag: Seguir una etiqueta\n    more: Más\n  personal:\n    overview: Información general\n    answers: Respuestas\n    answer: respuesta\n    questions: Preguntas\n    question: pregunta\n    bookmarks: Guardadas\n    reputation: Reputación\n    comments: Comentarios\n    votes: Votos\n    badges: Insignias\n    newest: Más reciente\n    score: Puntuación\n    edit_profile: Editar perfil\n    visited_x_days: \"Visitado {{ count }} días\"\n    viewed: Visto\n    joined: Unido\n    comma: \",\"\n    last_login: Visto\n    about_me: Sobre mí\n    about_me_empty: \"// ¡Hola Mundo!\"\n    top_answers: Mejores respuestas\n    top_questions: Preguntas Principales\n    stats: Estadísticas\n    list_empty: No se encontraron publicaciones.<br />¿Quizás le gustaría seleccionar una pestaña diferente?\n    content_empty: No se han encontrado publicaciones.\n    accepted: Aceptada\n    answered: respondida\n    asked: preguntó\n    downvoted: votado negativamente\n    mod_short: MOD\n    mod_long: Moderadores\n    x_reputation: reputación\n    x_votes: votos recibidos\n    x_answers: respuestas\n    x_questions: preguntas\n    recent_badges: Insignias recientes\n  install:\n    title: Instalación\n    next: Próximo\n    done: Hecho\n    config_yaml_error: No se puede crear el archivo config.yaml.\n    lang:\n      label: Elige un idioma\n    db_type:\n      label: Motor de base de datos\n    db_username:\n      label: Nombre de usuario\n      placeholder: raíz\n      msg: El nombre de usuario no puede estar vacío.\n    db_password:\n      label: Contraseña\n      placeholder: raíz\n      msg: La contraseña no puede estar vacía.\n    db_host:\n      label: Host de base de datos\n      placeholder: \"db:3306\"\n      msg: El host de base de datos no puede estar vacío.\n    db_name:\n      label: Nombre de base de datos\n      placeholder: respuesta\n      msg: El nombre de la base de datos no puede estar vacío.\n    db_file:\n      label: Archivo de base de datos\n      placeholder: /data/respuesta.db\n      msg: El archivo de la base de datos no puede estar vacío.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Crear config.yaml\n      label: El archivo config.yaml creado.\n      desc: >-\n        Puede crear el archivo <1>config.yaml</1> manualmente en el directorio <1>/var/www/xxx/</1> y pegar el siguiente texto en él.\n      info: Después de haber hecho eso, haga clic en el botón \"Siguiente\".\n    site_information: Información del sitio\n    admin_account: Cuenta de administrador\n    site_name:\n      label: Nombre del sitio\n      msg: El nombre del sitio no puede estar vacío.\n      msg_max_length: El nombre del sitio tener como máximo 30 caracteres.\n    site_url:\n      label: Sitio URL\n      text: La dirección de su sitio.\n      msg:\n        empty: La URL del sitio no puede estar vacía.\n        incorrect: Formato incorrecto de la URL del sitio.\n        max_length: El URL del sitio debe tener como máximo 512 caracteres.\n    contact_email:\n      label: Correo electrónico de contacto\n      text: Dirección de correo electrónico del contacto clave responsable de este sitio.\n      msg:\n        empty: El correo electrónico de contacto no puede estar vacío.\n        incorrect: Formato incorrecto de correo electrónico de contacto.\n    login_required:\n      label: Privado\n      switch: Inicio de sesión requerido\n      text: Solo usuarios conectados pueden acceder a esta comunidad.\n    admin_name:\n      label: Nombre\n      msg: El nombre no puede estar vacío.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Contraseña\n      text: >-\n        Necesitará esta contraseña para iniciar sesión. Guárdela en un lugar seguro.\n      msg: La contraseña no puede estar vacía.\n      msg_min_length: La contraseña debe contener 8 caracteres como mínimo.\n      msg_max_length: La contraseña debe contener como máximo 32 caracteres.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Correo electrónico\n      text: Necesitará este correo electrónico para iniciar sesión.\n      msg:\n        empty: El correo electrónico no puede estar vacío.\n        incorrect: Correo electrónico con formato incorrecto.\n    ready_title: Tu sitio está listo\n    ready_desc: >-\n      Si alguna vez desea cambiar más configuraciones, visite la <1>sección de administración</1>; encuéntrelo en el menú del sitio.\n    good_luck: \"¡Diviértete y buena suerte!\"\n    warn_title: Advertencia\n    warn_desc: >-\n      El archivo <1>config.yaml</1> ya existe. Si necesita restablecer alguno de los elementos de configuración de este archivo, elimínelo primero.\n    install_now: Puede intentar <1>instalar ahora</1>.\n    installed: Ya instalado\n    installed_desc: >-\n      Parece que ya lo has instalado. Para reinstalar, borre primero las tablas de la base de datos anterior.\n    db_failed: La conexión a la base de datos falló\n    db_failed_desc: >-\n      Esto significa que la información de la base de datos en tu archivo <1>config.yaml</1> es incorrecta o que no pudo establecerse contacto con el servidor de la base de datos. Esto podría significar que el host está caído.\n  counts:\n    views: puntos de vista\n    votes: votos\n    answers: respuestas\n    accepted: Aceptado\n  page_error:\n    http_error: Error HTTP {{ code }}\n    desc_403: No tienes permiso para acceder a esta página.\n    desc_404: Desafortunadamente, esta página no existe.\n    desc_50X: Se produjo un error en el servidor y no pudo completarse tu solicitud.\n    back_home: Volver a la página de inicio\n  page_maintenance:\n    desc: \"Estamos en mantenimiento, pronto estaremos de vuelta.\"\n  nav_menus:\n    dashboard: Panel\n    contents: Contenido\n    questions: Preguntas\n    answers: Respuestas\n    users: Usuarios\n    badges: Insignias\n    flags: Banderas\n    settings: Ajustes\n    general: General\n    interface: Interfaz\n    smtp: SMTP\n    branding: Marca\n    legal: Legal\n    write: Escribir\n    terms: Terms\n    tos: Términos de servicio\n    privacy: Privacidad\n    seo: ESTE\n    customize: Personalizar\n    themes: Temas\n    login: Iniciar sesión\n    privileges: Privilegios\n    plugins: Extensiones\n    installed_plugins: Extensiones Instaladas\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Bienvenido a {{site_name}}\n  user_center:\n    login: Iniciar sesión\n    qrcode_login_tip: Por favor utiliza {{ agentName }} para escanear el código QR e iniciar sesión.\n    login_failed_email_tip: Error al iniciar sesión, por favor permite el acceso a tu información de correo de esta aplicación antes de intentar nuevamente.\n  badges:\n    modal:\n      title: Enhorabuena\n      content: Has ganado una nueva insignia.\n      close: Cerrar\n      confirm: Ver insignia\n    title: Insignias\n    awarded: Premiado\n    earned_×: Obtenidos ×{{ number }}\n    ×_awarded: \"{{ number }} adjudicado\"\n    can_earn_multiple: Puedes ganar esto varias veces.\n    earned: Ganado\n  admin:\n    admin_header:\n      title: Administrador\n    dashboard:\n      title: Panel\n      welcome: '¡Bienvenido a Admin!'\n      site_statistics: Estadísticas del sitio\n      questions: \"Preguntas:\"\n      resolved: \"Resuelto:\"\n      unanswered: \"Sin respuesta:\"\n      answers: \"Respuestas:\"\n      comments: \"Comentarios:\"\n      votes: \"Votos:\"\n      users: \"Usuarios:\"\n      flags: \"Banderas:\"\n      reviews: \"Revisar:\"\n      site_health: Salud del sitio\n      version: \"Versión:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Cargar carpeta:\"\n      run_mode: \"Modo de ejecución:\"\n      private: Privado\n      public: Público\n      smtp: \"SMTP:\"\n      timezone: \"Zona horaria:\"\n      system_info: Información del sistema\n      go_version: \"Versión de Go:\"\n      database: \"Base de datos:\"\n      database_size: \"Tamaño de la base de datos:\"\n      storage_used: \"Almacenamiento utilizado:\"\n      uptime: \"Tiempo ejecutándose:\"\n      links: Enlaces\n      plugins: Extensiones\n      github: GitHub\n      blog: Blog\n      contact: Contacto\n      forum: Foro\n      documents: Documentos\n      feedback: Comentario\n      support: Soporte\n      review: Revisar\n      config: Configuración\n      update_to: Actualizar para\n      latest: Lo más nuevo\n      check_failed: Comprobación fallida\n      \"yes\": \"Si\"\n      \"no\": \"No\"\n      not_allowed: No permitido\n      allowed: Permitido\n      enabled: Activado\n      disabled: Desactivado\n      writable: Redactable\n      not_writable: No redactable\n    flags:\n      title: Banderas\n      pending: Pendiente\n      completed: Terminado\n      flagged: Marcado\n      flagged_type: Reportado {{ type }}\n      created: Creado\n      action: Acción\n      review: Revisar\n    user_role_modal:\n      title: Cambiar rol de usuario a...\n      btn_cancel: Cancelar\n      btn_submit: Entregar\n    new_password_modal:\n      title: Establecer nueva contraseña\n      form:\n        fields:\n          password:\n            label: Contraseña\n            text: El usuario será desconectado y deberá iniciar sesión nuevamente.\n            msg: La contraseña debe contener entre 8 y 32 caracteres de longitud.\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    edit_profile_modal:\n      title: Editar perfil\n      form:\n        fields:\n          display_name:\n            label: Nombre para mostrar\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Nombre de usuario\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Correo electrónico\n            msg_invalid: Dirección de correo inválida.\n      edit_success: Editado exitosamente\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    user_modal:\n      title: Añadir nuevo usuario\n      form:\n        fields:\n          users:\n            label: Añadir usuarios en cantidad\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separe “nombre, correo electrónico, contraseña” con comas. Un usuario por línea.\n            msg: \"Por favor, introduzca el correo electrónico del usuario, uno por línea.\"\n          display_name:\n            label: Nombre público\n            msg: El nombre de la pantalla debe tener entre 2 y 30 caracteres de longitud.\n          email:\n            label: Correo\n            msg: El correo no es válido.\n          password:\n            label: Contraseña\n            msg: La contraseña debe contener entre 8 y 32 caracteres de longitud.\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    users:\n      title: Usuarios\n      name: Nombre\n      email: Correo electrónico\n      reputation: Reputación\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Estado\n      role: Rol\n      action: Acción\n      change: Cambiar\n      all: Todo\n      staff: Personal\n      more: Más\n      inactive: Inactivo\n      suspended: Suspendido\n      deleted: Eliminado\n      normal: Normal\n      Moderator: Moderador\n      Admin: Administrador\n      User: Usuario\n      filter:\n        placeholder: \"Filtrar por nombre, usuario:id\"\n      set_new_password: Establecer nueva contraseña\n      edit_profile: Editar perfil\n      change_status: Cambiar Estado\n      change_role: Cambiar rol\n      show_logs: Mostrar registros\n      add_user: Agregar usuario\n      deactivate_user:\n        title: Desactivar usuario\n        content: Un usuario inactivo debe revalidar su correo electrónico.\n      delete_user:\n        title: Eliminar este usuario\n        content: '¿Estás seguro de que deseas eliminar este usuario? ¡Esto es permanente!'\n        remove: Eliminar su contenido\n        label: Eliminar todas las preguntas, respuestas, comentarios, etc.\n        text: No marque esto si solo desea eliminar la cuenta del usuario.\n      suspend_user:\n        title: Suspender a este usuario\n        content: Un usuario suspendido no puede iniciar sesión.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Preguntas\n      unlisted: No listado\n      post: Correo\n      votes: Votos\n      answers: Respuestas\n      created: Creado\n      status: Estado\n      action: Acción\n      change: Cambiar\n      pending: Pendiente\n      filter:\n        placeholder: \"Filtrar por título, pregunta:id\"\n    answers:\n      page_title: Respuestas\n      post: Correo\n      votes: Votos\n      created: Creado\n      status: Estado\n      action: Acción\n      change: Cambiar\n      filter:\n        placeholder: \"Filtrar por título, respuesta: id\"\n    general:\n      page_title: General\n      name:\n        label: Nombre del sitio\n        msg: El nombre del sitio no puede estar vacío.\n        text: \"El nombre de este sitio, tal como se usa en la etiqueta del título.\"\n      site_url:\n        label: Sitio URL\n        msg: La url del sitio no puede estar vacía.\n        validate: Por favor introduzca un URL válido.\n        text: La dirección de su sitio.\n      short_desc:\n        label: Descripción breve del sitio\n        msg: La descripción breve del sitio no puede estar vacía.\n        text: \"Breve descripción, tal como se usa en la etiqueta del título en la página de inicio.\"\n      desc:\n        label: Descripción del sitio\n        msg: La descripción del sitio no puede estar vacía.\n        text: \"Describa este sitio en una oración, como se usa en la etiqueta de meta descripción.\"\n      contact_email:\n        label: Correo electrónico de contacto\n        msg: El correo electrónico de contacto no puede estar vacío.\n        validate: El correo electrónico de contacto no es válido.\n        text: Dirección de correo electrónico del contacto clave responsable de este sitio.\n      check_update:\n        label: Actualizaciones de software\n        text: Comprobar actualizaciones automáticamente\n    interface:\n      page_title: Interfaz\n      language:\n        label: Idioma de Interfaz\n        msg: El idioma de la interfaz no puede estar vacío.\n        text: Idioma de la interfaz de usuario. Cambiará cuando actualice la página.\n      time_zone:\n        label: Zona horaria\n        msg: El huso horario no puede estar vacío.\n        text: Elija una ciudad en la misma zona horaria que usted.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Desde correo\n        msg: Desde el correo electrónico no puede estar vacío.\n        text: La dirección de correo electrónico desde la que se envían los correos electrónicos.\n      from_name:\n        label: Desde nombre\n        msg: Desde el nombre no puede estar vacío.\n        text: El nombre desde el que se envían los correos electrónicos.\n      smtp_host:\n        label: Host SMTP\n        msg: El host SMTP no puede estar vacío.\n        text: Su servidor de correo.\n      encryption:\n        label: Cifrado\n        msg: El cifrado no puede estar vacío.\n        text: Para la mayoría de los servidores, SSL es la opción recomendada.\n        ssl: SSL\n        tls: TLS\n        none: Ninguno\n      smtp_port:\n        label: Puerto SMTP\n        msg: El puerto SMTP debe ser el número 1 ~ 65535.\n        text: El puerto a su servidor de correo.\n      smtp_username:\n        label: Nombre de usuario SMTP\n        msg: El nombre de usuario SMTP no puede estar vacío.\n      smtp_password:\n        label: Contraseña de SMTP\n        msg: La contraseña SMTP no puede estar vacía.\n      test_email_recipient:\n        label: Destinatarios de correo electrónico de prueba\n        text: Proporcione la dirección de correo electrónico que recibirá los envíos de prueba.\n        msg: Los destinatarios de correo electrónico de prueba no son válidos\n      smtp_authentication:\n        label: Habilitar autenticación\n        title: Autenticación SMTP\n        msg: La autenticación SMTP no puede estar vacía.\n        \"yes\": \"Si\"\n        \"no\": \"No\"\n    branding:\n      page_title: Marca\n      logo:\n        label: Logo\n        msg: El logotipo no puede estar vacío.\n        text: La imagen del logotipo en la parte superior izquierda de su sitio. Utilice una imagen rectangular ancha con una altura de 56 y una relación de aspecto superior a 3:1. Si se deja en blanco, se mostrará el texto del título del sitio.\n      mobile_logo:\n        label: Logo Móvil\n        text: El logotipo utilizado en la versión móvil de su sitio. Utilice una imagen rectangular ancha con una altura de 56. Si se deja en blanco, se utilizará la imagen de la configuración de \"logotipo\".\n      square_icon:\n        label: Icono cuadrado\n        msg: El icono cuadrado no puede estar vacío.\n        text: Imagen utilizada como base para los iconos de metadatos. Idealmente, debería ser más grande que 512x512.\n      favicon:\n        label: Icono de favoritos\n        text: Un favicon para su sitio. Para que funcione correctamente sobre un CDN, debe ser un png. Se cambiará el tamaño a 32x32. Si se deja en blanco, se utilizará el \"icono cuadrado\".\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Términos de servicio\n        text: \"Puede agregar términos de contenido de servicio aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí.\"\n      privacy_policy:\n        label: Política de privacidad\n        text: \"Puede agregar contenido de política de privacidad aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Escribir respuesta\n        label: Cada usuario solo puede escribir una respuesta por pregunta\n        text: \"Desactivar para permitir a los usuarios escribir múltiples respuestas a la misma pregunta, lo que puede causar que las respuestas no estén enfocadas.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Etiquetas recomendadas\n        text: \"Las etiquetas recomendadas se mostrarán en la lista desplegable por defecto.\"\n        msg:\n          contain_reserved: \"las etiquetas recomendadas no pueden contener etiquetas reservadas\"\n      required_tag:\n        title: Establecer etiquetas necesarias\n        label: Establecer \"Etiquetas recomendadas\" como etiquetas requeridas\n        text: \"Cada nueva pregunta debe tener al menos una etiqueta de recomendación.\"\n      reserved_tags:\n        label: Etiquetas reservadas\n        text: \"Las etiquetas reservadas sólo pueden ser usadas por el moderador.\"\n      image_size:\n        label: Tamaño máximo de la imagen (MB)\n        text: \"Tamaño máximo de la imagen.\"\n      attachment_size:\n        label: Tamaño máximo del archivo adjunto (MB)\n        text: \"El tamaño máximo de subida de archivos adjuntos.\"\n      image_megapixels:\n        label: Megapixels de imagen máx\n        text: \"Número máximo de megapixels permitidos para una imagen.\"\n      image_extensions:\n        label: Extensiones de adjuntos autorizadas\n        text: \"Una lista de extensiones de archivo permitidas para la visualización de imágenes, separadas con comas.\"\n      attachment_extensions:\n        label: Extensiones de adjuntos autorizadas\n        text: \"Una lista de extensiones de archivo permitidas para subir, separadas con comas. ADVERTENCIA: Permitir subidas puede causar problemas de seguridad.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Enlace permanente\n        text: Las estructuras de URL personalizadas pueden mejorar la facilidad de uso y la compatibilidad futura de sus enlaces.\n      robots:\n        label: robots.txt\n        text: Esto anulará permanentemente cualquier configuración del sitio relacionada.\n    themes:\n      page_title: Temas\n      themes:\n        label: Temas\n        text: Seleccione un tema existente.\n      color_scheme:\n        label: Esquema de color\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Color primario\n        text: Modifica los colores usados por tus temas\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS y HTML\n      custom_css:\n        label: CSS personalizado\n        text: >\n\n      head:\n        label: Cabeza\n        text: >\n\n      header:\n        label: Encabezado\n        text: >\n\n      footer:\n        label: Pie de página\n        text: Esto se insertará antes </body>.\n      sidebar:\n        label: Barra lateral\n        text: Esto se añadirá en la barra lateral.\n    login:\n      page_title: Iniciar sesión\n      membership:\n        title: Membresía\n        label: Permitir registro de nuevas ceuntas\n        text: Desactiva esto para evitar que cualquier persona pueda crear una cuenta.\n      email_registration:\n        title: Registro de correo electrónico\n        label: Permitir registro de correo electrónico\n        text: Desactivar para evitar registros a través de correo electrónico.\n      allowed_email_domains:\n        title: Dominios de correo electrónico permitidos\n        text: Dominios de correo electrónico con los que los usuarios deben registrar sus cuentas. Un dominio por línea. Ignorado cuando esté vacío.\n      private:\n        title: Privado\n        label: Inicio de sesión requerido\n        text: Sólo usuarios con sesión iniciada pueden acceder a esta comunidad.\n      password_login:\n        title: Inicio de sesión con contraseña\n        label: Permitir inicio de sesión con correo y contraseña\n        text: \"ADVERTENCIA: Si se desactiva, es posible que no pueda iniciar sesión si no ha configurado previamente otro método de inicio de sesión.\"\n    installed_plugins:\n      title: Extensiones Instaladas\n      plugin_link: Los plugins extienden y expanden la funcionalidad. Puede encontrar plugins en el <1>Repositorio de plugin</1>.\n      filter:\n        all: Todos\n        active: Activo\n        inactive: Inactivo\n        outdated: Desactualizado\n      plugins:\n        label: Extensiones\n        text: Seleccione una extensión existente.\n      name: Nombre\n      version: Versión\n      status: Estado\n      action: Acción\n      deactivate: Desactivar\n      activate: Activar\n      settings: Ajustes\n    settings_users:\n      title: Usuarios\n      avatar:\n        label: Avatar predeterminado\n        text: Para usuarios sin un avatar personalizado propio.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL de la base API del proveedor Gravatar. Ignorado cuando esté vacío.\n      profile_editable:\n        title: Perfil editable\n      allow_update_display_name:\n        label: Permitir a usuarios cambiar su nombre público\n      allow_update_username:\n        label: Permitir a los usuarios cambiar su nombre de usuario\n      allow_update_avatar:\n        label: Permitir a los usuarios cambiar su foto de perfil\n      allow_update_bio:\n        label: Permitir a los usuarios cambiar su descripción\n      allow_update_website:\n        label: Permitir a los usuarios cambiar su sitio web\n      allow_update_location:\n        label: Permitir a los usuarios cambiar su ubicación\n    privilege:\n      title: Privilegios\n      level:\n        label: Nivel de reputación requerido\n        text: Elegir reputación requerida para los privilegios\n      msg:\n        should_be_number: la entrada debe ser número\n        number_larger_1: número debe ser igual o mayor que 1\n    badges:\n      action: Accin\n      active: Activo\n      activate: Activación\n      all: All\n      awards: Premios\n      deactivate: Desactivar\n      filter:\n        placeholder: Filtrar por nombre, insignia:id\n      group: Grupo\n      inactive: Inactivo\n      name: Nombre\n      show_logs: Mostrar logs\n      status: Status\n      title: Insignias\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (opcional)\n    empty: no puede estar en blanco\n    invalid: no es válido\n    btn_submit: Guardar\n    not_found_props: \"La propiedad requerida {{ key }} no se ha encontrado.\"\n    select: Seleccionar\n  page_review:\n    review: Revisar\n    proposed: propuesto\n    question_edit: Edición de preguntas\n    answer_edit: Edición de respuestas\n    tag_edit: Edición de etiquetas\n    edit_summary: Editar resumen\n    edit_question: Editar pregunta\n    edit_answer: Editar respuesta\n    edit_tag: Editar etiqueta\n    empty: No quedan tareas de revisión.\n    approve_revision_tip: '¿Aprueban ustedes esta revisión?'\n    approve_flag_tip: '¿Aprueban ustedes esta bandera?'\n    approve_post_tip: '¿Aprueban ustedes esta bandera?'\n    approve_user_tip: '¿Apruebas a este usuario?'\n    suggest_edits: Ediciones Sugeridas\n    flag_post: Marcar publicación\n    flag_user: Marcar usuario\n    queued_post: Publicación en cola\n    queued_user: Usuario en cola\n    filter_label: Tipo\n    reputation: reputación\n    flag_post_type: Marcada esta publicación como {{ type }}.\n    flag_user_type: Marcado este usuario como {{ type }}.\n    edit_post: Editar publicación\n    list_post: Listar publicación\n    unlist_post: Deslistar publicación\n  timeline:\n    undeleted: recuperado\n    deleted: eliminado\n    downvote: voto negativo\n    upvote: votar a favor\n    accept: aceptar\n    cancelled: cancelado\n    commented: comentado\n    rollback: retroceder\n    edited: editada\n    answered: contestada\n    asked: preguntó\n    closed: cerrado\n    reopened: reabierto\n    created: creado\n    pin: fijado\n    unpin: desfijado\n    show: listado\n    hide: deslistado\n    title: \"Historial para\"\n    tag_title: \"Línea temporal para\"\n    show_votes: \"Mostrar votos\"\n    n_or_a: N/A\n    title_for_question: \"Línea de tiempo para\"\n    title_for_answer: \"Cronología de la respuesta a {{ title }} por {{ author }}\"\n    title_for_tag: \"Cronología de la etiqueta\"\n    datetime: Fecha y hora\n    type: Tipo\n    by: Por\n    comment: Comentario\n    no_data: \"No pudimos encontrar nada.\"\n  users:\n    title: Usuarios\n    users_with_the_most_reputation: Usuarios con el mayor puntaje de reputación esta semana\n    users_with_the_most_vote: Usuarios que más votaron esta semana\n    staffs: Nuestor equipo de la comunidad\n    reputation: reputación\n    votes: votos\n  prompt:\n    leave_page: '¿Seguro que quieres salir de la página?'\n    changes_not_save: Es posible que sus cambios no se guarden.\n  draft:\n    discard_confirm: '¿Está seguro de que desea descartar este borrador?'\n  messages:\n    post_deleted: Esta publicación ha sido eliminada.\n    post_cancel_deleted: Este publicación ha sido restaurada.\n    post_pin: Esta publicación ha sido fijada.\n    post_unpin: Esta publicación ha sido desfijada.\n    post_hide_list: Esta publicación ha sido ocultada de la lista.\n    post_show_list: Esta publicación ha sido mostrada a la lista.\n    post_reopen: Esta publicación ha sido reabierta.\n    post_list: Esta publicación ha sido listada.\n    post_unlist: Esta publicación ha sido retirado de la lista..\n    post_pending: Su publicación está pendiente de revisión. Esto es una vista previa, será visible después de que haya sido aprobado.\n    post_closed: Esta publicación ha sido cerrada.\n    answer_deleted: Esta respuesta ha sido eliminada.\n    answer_cancel_deleted: Esta respuesta ha sido restaurada.\n    change_user_role: El rol de este usuario ha sido cambiado.\n    user_inactive: Este usuario ya esta inactivo.\n    user_normal: Este usuario ya es normal.\n    user_suspended: Este usuario ha sido suspendido.\n    user_deleted: Este usuario ha sido eliminado.\n    user_added: User has been added successfully.\n    badge_activated: Esta insignia ha sido activada.\n    badge_inactivated: Esta insignia ha sido desactivada.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/fa_IR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: موفق.\n    unknown:\n      other: خطای ناشناخته.\n    request_format_error:\n      other: ساختار درخواست شناخته شده نیست.\n    unauthorized_error:\n      other: دسترسی غیر مجاز.\n    database_error:\n      other: خطای سرور داده.\n    forbidden_error:\n      other: عدم اجازه دسترسی.\n    duplicate_request_error:\n      other: ارسال تکراری.\n  action:\n    report:\n      other: نشان\n    edit:\n      other: ویرایش\n    delete:\n      other: حذف\n    close:\n      other: بستن\n    reopen:\n      other: بازگشایی\n    forbidden_error:\n      other: عدم اجازه دسترسی.\n    pin:\n      other: سنجاق کردن\n    hide:\n      other: پنهان کردن\n    unpin:\n      other: برداشتن سنجاق\n    show:\n      other: فهرست\n    invite_someone_to_answer:\n      other: ویرایش\n    undelete:\n      other: بازگردانی حذف\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: کاربر\n      admin:\n        other: ادمین\n      moderator:\n        other: مدير\n    description:\n      user:\n        other: پیش فرض بدون دسترسی خاص.\n      admin:\n        other: تمامی دسترسی ها را داراست.\n      moderator:\n        other: دسترسی به تمامی پست هارا داراست بجز تنظیمات ادمین.\n  privilege:\n    level_1:\n      description:\n        other: سطح ۱ (شهرت کمی نیاز هست برای تیم/گروه های خصوصی)\n    level_2:\n      description:\n        other: سطح ۲ (شهرت کمی نیاز هست برای انجمن های استارتاپی)\n    level_3:\n      description:\n        other: سطح ۳ (شهرت بالایی برای نیاز هست برای انجمن های تکمیل)\n    level_custom:\n      description:\n        other: سطح دلخواه\n    rank_question_add_label:\n      other: سوال بپرس\n    rank_answer_add_label:\n      other: جواب بده\n    rank_comment_add_label:\n      other: نظر بده\n    rank_report_add_label:\n      other: نشان\n    rank_comment_vote_up_label:\n      other: رای موافق\n    rank_link_url_limit_label:\n      other: بیشتر از دو لینک را هم زمان پست کنید\n    rank_question_vote_up_label:\n      other: رای موافق\n    rank_answer_vote_up_label:\n      other: رای موافق\n    rank_question_vote_down_label:\n      other: رای مخالف\n    rank_answer_vote_down_label:\n      other: رای مخالف\n    rank_invite_someone_to_answer_label:\n      other: فردی رو دعوت کنین تا جواب بدن\n    rank_tag_add_label:\n      other: ساخت تگ جدید\n    rank_tag_edit_label:\n      other: ویرایش توضیحات تگ (نیازمند بازبینی)\n    rank_question_edit_label:\n      other: ویرایش سوال دیگران (نیازمند بازبینی)\n    rank_answer_edit_label:\n      other: ویرایش جواب دیگران (نیازمند بازبینی)\n    rank_question_edit_without_review_label:\n      other: ویرایش سوال دیگران بدون نیاز به بازبینی\n    rank_answer_edit_without_review_label:\n      other: ویرایش جواب دیگران بدون نیاز به بازبینی\n    rank_question_audit_label:\n      other: بازبینی ویرایش های سوال\n    rank_answer_audit_label:\n      other: بازبینی ویرایش های جواب\n    rank_tag_audit_label:\n      other: بازبینی ویرایش های تگ\n    rank_tag_edit_without_review_label:\n      other: ویرایش توضیحات تگ بدون بازبینی\n    rank_tag_synonym_label:\n      other: مدیریت تگ های مترادف\n  email:\n    other: ایمیل\n  e_mail:\n    other: ایمیل\n  password:\n    other: رمز\n  pass:\n    other: رمز\n  old_pass:\n    other: Current password\n  original_text:\n    other: پست جاری\n  email_or_password_wrong_error:\n    other: ایمیل و رمز وارد شده صحیح نیست.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: رمز عبور نمی تواند شامل فضای خالی باشد.\n    admin:\n      cannot_update_their_password:\n        other: نمیتوانید رمز عبور خود را تغییر دهید.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: نمیتوانید وضعیت خود را تغییر دهید.\n      email_or_password_wrong:\n        other: ایمیل و رمز وارد شده صحیح نیست.\n    answer:\n      not_found:\n        other: جواب پیدا نشد.\n      cannot_deleted:\n        other: اجازه حذف ندارید.\n      cannot_update:\n        other: اجازه بروزرسانی ندارید.\n      question_closed_cannot_add:\n        other: سوالات بسته شده اند و نمیتوان سوالی اضافه کرد.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: نظرات قابل ویرایش نیستند.\n      not_found:\n        other: نظر پیدا نشد.\n      cannot_edit_after_deadline:\n        other: زمان زیادی برای ویرایش نظر گذشته است.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: ایمیل تکراری.\n      need_to_be_verified:\n        other: ایمیل باید تایید شود.\n      verify_url_expired:\n        other: لینک تایید ایمیل منقضی شده است،‌لطفا دوباره تلاش کنید.\n      illegal_email_domain_error:\n        other: دامنه ایمیل پیشتیبانی نمی شود، لطفا از ایمیل دیگری استفاده کنید.\n    lang:\n      not_found:\n        other: فایل زبان یافت نشد.\n    object:\n      captcha_verification_failed:\n        other: اشتباه در Captcha.\n      disallow_follow:\n        other: شما اجازه فالو کردن ندارید.\n      disallow_vote:\n        other: شما اجازه رای دادن ندارید.\n      disallow_vote_your_self:\n        other: شما نمی توانید به پست خودتان رای دهید.\n      not_found:\n        other: آبجکت مورد نظر پیدا نشد.\n      verification_failed:\n        other: تایید با خطا مواجه شد.\n      email_or_password_incorrect:\n        other: ایمیل و رمز وارد شده صحیح نیست.\n      old_password_verification_failed:\n        other: پسورد قدیمی تایید نشد\n      new_password_same_as_previous_setting:\n        other: پسورد جدید با پسورد قدیمی یکسان است.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: این پست حذف شده است.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: سوال پیدا نشد.\n      cannot_deleted:\n        other: اجازه حذف ندارید.\n      cannot_close:\n        other: اجاره بستن ندارید.\n      cannot_update:\n        other: اجازه بروزرسانی ندارید.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: شهرت ناکافی.\n      vote_fail_to_meet_the_condition:\n        other: ممنون بابت بازخورد. شما حداقل به {{.Rank}} نیاز دارید برای رای دادن.\n      no_enough_rank_to_operate:\n        other: شما حداقل به {{.Rank}} نیاز دارید برای انجام این کار.\n    report:\n      handle_failed:\n        other: گزارش دهی با مشکل مواجه شد.\n      not_found:\n        other: گزارش مورد نظر پیدا نشد.\n    tag:\n      already_exist:\n        other: تگ از قبل موجود است.\n      not_found:\n        other: تگ پیدا نشد.\n      recommend_tag_not_found:\n        other: تگ پیشنهاد شده موجود نیست.\n      recommend_tag_enter:\n        other: لطفا حداقل یک تگ را وارد کنید.\n      not_contain_synonym_tags:\n        other: نباید تگ مترادف داشته باشد.\n      cannot_update:\n        other: اجازه بروزرسانی ندارید.\n      is_used_cannot_delete:\n        other: نمی توانید تگی که در حال استفاده است را حذف کنید.\n      cannot_set_synonym_as_itself:\n        other: شما نمی توانید مترادفی برای برچسب فعلی به عوان خودش تنظیم کنین.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: '\"از طرفه\" نمی تواند آدرس ایمیل باشد.'\n    theme:\n      not_found:\n        other: تم پیدا نشد.\n    revision:\n      review_underway:\n        other: فعلا امکان ویرایش وجود ندارد،‌این نسخه در صف بازینی قرار دارد.\n      no_permission:\n        other: اجاره بازبینی و اصلاح ندارید.\n    user:\n      external_login_missing_user_id:\n        other: پلتفورم های سوم شخص نمی توانند نام کاربر خاصی را ارائه دهند، بنابر این شما نمی توانید وارد شوید، لطفا با مدیریت وبسایت تماس بگیرید.\n      external_login_unbinding_forbidden:\n        other: لطفاً یک رمز ورود برای حساب خود قبل از حذف تنظیم کنید.\n      email_or_password_wrong:\n        other:\n          other: ایمیل و رمز وارد شده صحیح نیست.\n      not_found:\n        other: کاربر پیدا نشد.\n      suspended:\n        other: کاربر در حالت تعلیق قرار داده شده است.\n      username_invalid:\n        other: نام کاربری نامعتبر است.\n      username_duplicate:\n        other: این نام کاربری قبلا استفاده شده است.\n      set_avatar:\n        other: ست کردن آواتار با مشکل مواجه شد.\n      cannot_update_your_role:\n        other: شما نمی توانید وظیفه خود را تغییر دهید.\n      not_allowed_registration:\n        other: درحال حاضر سایت برای ثبت نام باز نیست.\n      not_allowed_login_via_password:\n        other: در حال حاضر سایت اجازه ورود از طریق رمز عبور ندارد.\n      access_denied:\n        other: دسترسی مجاز نیست\n      page_access_denied:\n        other: شما وجوز دسترسی به این صفحه را ندارید.\n      add_bulk_users_format_error:\n        other: \"مشکل پیش آمده در فرمت {{.Field}} در کنار {{.Content}} در خط {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"تعداد کاربرانی که اضافه می کنید باید رنج بین ۱-{{.MaxAmount}} باشند.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: خواندن کافیگ با مشکل مواجه شد\n    database:\n      connection_failed:\n        other: اتصال به دیتابیس موفقیت آمیز نبود\n      create_table_failed:\n        other: ایجاد کردن جدول موفقیت آمیز نبود\n    install:\n      create_config_failed:\n        other: فایل config.yaml نمی تواند ایجاد شود.\n    upload:\n      unsupported_file_format:\n        other: فرمت فایل پشتیبانی نمی شود.\n    site_info:\n      config_not_found:\n        other: پیکربندی سایت پیدا نشد.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: هرزنامه\n      desc:\n        other: این پست یک تبلیغ یا خرابکاری است. این پس مفید یا مربوط به این موضوع نمی باشد.\n    rude_or_abusive:\n      name:\n        other: بی ادب یا توهین آمیز\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: تکراری\n      desc:\n        other: این سوال قبلا پرسیده و جواب داده شده است.\n      placeholder:\n        other: لینک سوال مورد نظر را وارد کنید\n    not_a_answer:\n      name:\n        other: این یک پاسخ نیست\n      desc:\n        other: \".\"\n    no_longer_needed:\n      name:\n        other: دیگر نیازی نیست\n      desc:\n        other: این نظر منسوخ شده، مکلامه ای یا مربوط به این پس نیست.\n    something:\n      name:\n        other: یک مورد دیگر\n      desc:\n        other: این پست به دلیل دیگری که در بالا ذکر نشده نیاز به توجه کارکنان دارد.\n      placeholder:\n        other: به طور خاص به ما اطلاع دهید که در مورد چه چیزی نگران هستید\n    community_specific:\n      name:\n        other: یک دلیل خاص جامعه\n      desc:\n        other: این سوال با دستورالعمل جامعه مطابقت ندارد.\n    not_clarity:\n      name:\n        other: نیاز به جزئیات یا واضح کردن دارد\n      desc:\n        other: این سوال درحال حاضر شامل چندتا سوال در یکی هست. باید فقط روی یک مشکل تمرکز کند.\n    looks_ok:\n      name:\n        other: به نظر خوب میاد\n      desc:\n        other: این پست همانطور که هست خوب است و کیفیت پایینی ندارد.\n    needs_edit:\n      name:\n        other: نیاز به ویرایش بود، من انجام دادم\n      desc:\n        other: مشکلات این پست را خودتان بهبود و اصلاح کنید.\n    needs_close:\n      name:\n        other: نیاز است که بسته بشود\n      desc:\n        other: به یک سوال بسته شده نمیتوان جوابی ثبت کرد بلکه می توان ویرایش، رای و نظر داد.\n    needs_delete:\n      name:\n        other: نیاز است که حذف بشود\n      desc:\n        other: این پست حذف خواهد شد.\n  question:\n    close:\n      duplicate:\n        name:\n          other: هرزنامه\n        desc:\n          other: این سوال قبلا پرسیده و جواب داده شده است.\n      guideline:\n        name:\n          other: یک دلیل خاص جامعه\n        desc:\n          other: این سوال با دستورالعمل جامعه مطابقت ندارد.\n      multiple:\n        name:\n          other: نیاز به جزئیات یا واضح کردن دارد\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: یک مورد دیگر\n        desc:\n          other: این پست به دلیل دیگری نیاز دارد که در بالا ذکر نشده است.\n    operation_type:\n      asked:\n        other: پرسیده شده\n      answered:\n        other: جواب داده\n      modified:\n        other: تغییر یافته\n    deleted_title:\n      other: سوال حذف شده\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: سوال بارگزاری شده\n      answer_the_question:\n        other: سؤال جواب داده شده\n      update_answer:\n        other: جواب بارگذاری شده\n      accept_answer:\n        other: جواب پذیرفته شده\n      comment_question:\n        other: سوال از کامنت\n      comment_answer:\n        other: جواب از کامنت\n      reply_to_you:\n        other: به شما پاسخ داد\n      mention_you:\n        other: به شما اشاره کرده\n      your_question_is_closed:\n        other: سوال شما بسته شده است\n      your_question_was_deleted:\n        other: سوال شما حذف شده است\n      your_answer_was_deleted:\n        other: جواب شما حذف شده است\n      your_comment_was_deleted:\n        other: نظر شما پاک شده است\n      up_voted_question:\n        other: رای موافق\n      down_voted_question:\n        other: سوال با رای منفی\n      up_voted_answer:\n        other: پاسخ موافق\n      down_voted_answer:\n        other: جواب مخالف\n      up_voted_comment:\n        other: نظر بدون رای\n      invited_you_to_answer:\n        other: برای جواب دادن دعوت شده اید\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"آدرس ایمیل جدید خود را تایید کنید{{.SiteName}}\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} به سؤال شما پاسخ داد\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} شما را به پاسخ دعوت کرد\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} روی پست شما نظر داد\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] سؤال جدید: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] گذرواژه بازنشانی شد\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] حساب کاربری جدید خود را تأیید کنید\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] ایمیل آزمایشی\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: رأی مثبت\n    upvoted:\n      other: رأی مثبت\n    downvote:\n      other: رأی منفی\n    downvoted:\n      other: رأی منفی\n    accept:\n      other: پذیرفتن\n    accepted:\n      other: پذیرفته شده\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: نحوه فرمت کردن\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: قبلی\n    next: بعدی\n  page_title:\n    question: سوال\n    questions: سوالات\n    tag: برچسب\n    tags: برچسب ها\n    tag_wiki: ویکی تگ\n    create_tag: ایجاد برچسب\n    edit_tag: ویرایش برچسب\n    ask_a_question: Create Question\n    edit_question: ویرایش سوال\n    edit_answer: ویرایش پاسخ\n    search: جستجو\n    posts_containing: پست های شامل\n    settings: تنظیمات\n    notifications: اعلانات\n    login: ورود\n    sign_up: ثبت نام\n    account_recovery: بازیابی حساب کاربری\n    account_activation: فعالسازی حساب\n    confirm_email: تایید ایمیل\n    account_suspended: حساب تعلیق شد\n    admin: ادمین\n    change_email: نگارش ایمیل\n    install: نصب Bepors\n    upgrade: بروزرسانی بپرس\n    maintenance: تعمیر و نگهداری وب سایت\n    users: کاربرها\n    oauth_callback: در حال پردازش\n    http_404: خطای 404 HTTP\n    http_50X: خطای 500 HTTP\n    http_403: خطای 403 HTTP\n    logout: خروج\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: اعلانات\n    inbox: پیغام‌های دریافتی\n    achievement: دستاوردها\n    new_alerts: هشدار جدید\n    all_read: علامتگذاری همه بعنوان خوانده شده\n    show_more: نمایش بیشتر\n    someone: کسی\n    inbox_type:\n      all: همه\n      posts: پست ها\n      invites: دعوت ها\n      votes: آراء\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: حساب شما معلق شده است\n    until_time: \"حساب شما تا تاریخ {{ time }} به حالت تعلیق درآمده است.\"\n    forever: این کاربر برای همیشه به حالت تعلیق درآمده است.\n    end: شما یک دستورالعمل انجمن را رعایت نمی کنید.\n    contact_us: ارتباط با ما\n  editor:\n    blockquote:\n      text: مسابقه\n    bold:\n      text: قوی\n    chart:\n      text: نمودار\n      flow_chart: نمودار جریان\n      sequence_diagram: نمودار توالی\n      class_diagram: نمودار کلاس\n      state_diagram: نمودار حالت\n      entity_relationship_diagram: نمودار رابطه موجودیت\n      user_defined_diagram: نمودار تعریف شده توسط کاربر\n      gantt_chart: نمودار گانت\n      pie_chart: نمودار دابره‌ای\n    code:\n      text: نمونه کد\n      add_code: نمونه کد اضافه کنید\n      form:\n        fields:\n          code:\n            label: کد\n            msg:\n              empty: کد نمی تواند خالی باشد.\n          language:\n            label: زبان\n            placeholder: تشخیص خودکار\n      btn_cancel: لغو\n      btn_confirm: اضافه کردن\n    formula:\n      text: فرمول\n      options:\n        inline: فرمول در خط\n        block: بلاک کردن فرمول\n    heading:\n      text: سرفصل\n      options:\n        h1: سرفصل ۱\n        h2: سرفصل ۲\n        h3: سرفصل ۳\n        h4: سرفصل ۴\n        h5: سرفصل ۵\n        h6: سرفصل ۶\n    help:\n      text: راهنما\n    hr:\n      text: خط افقی\n    image:\n      text: عکس\n      add_image: افزودن عکس\n      tab_image: آپلود عکس\n      form_image:\n        fields:\n          file:\n            label: فایل عکس\n            btn: انتخاب عکس\n            msg:\n              empty: فایل نمی تواند خالی باشد.\n              only_image: فقط فایل های تصویری مجاز هستند.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: توضیحات\n      tab_url: لینک عکس\n      form_url:\n        fields:\n          url:\n            label: لینک عکس\n            msg:\n              empty: آدرس عکس نمی‌تواند خالی باشد.\n          name:\n            label: توضیحات\n      btn_cancel: لغو\n      btn_confirm: اضافه کردن\n      uploading: درحال ارسال\n    indent:\n      text: تورفتگی\n    outdent:\n      text: بیرون آمدگی\n    italic:\n      text: تاکید\n    link:\n      text: فراپیوند\n      add_link: اضافه کردن فراپیوند\n      form:\n        fields:\n          url:\n            label: آدرس\n            msg:\n              empty: آدرس نمی‌تواند خالی باشد.\n          name:\n            label: توضیح\n      btn_cancel: لغو\n      btn_confirm: افزودن\n    ordered_list:\n      text: فهرست عددی\n    unordered_list:\n      text: لیست گلوله‌ای\n    table:\n      text: جدول\n      heading: سرفصل\n      cell: تلفن همراه\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: این پست را می بندم بدلیل...\n    btn_cancel: لغو\n    btn_submit: فرستادن\n    remark:\n      empty: نمی‌تواند خالی باشد.\n    msg:\n      empty: لطفا یک دلیل را انتخاب کنید.\n  report_modal:\n    flag_title: من پرچم گذاری می کنم تا این پست را به عنوان گزارش کنم...\n    close_title: این پست را می بندم بدلیل...\n    review_question_title: بازبینی سوال\n    review_answer_title: بازبینی جواب\n    review_comment_title: بازبینی نظر\n    btn_cancel: لغو\n    btn_submit: ثبت\n    remark:\n      empty: نمی‌تواند خالی باشد.\n    msg:\n      empty: لطفا یک دلیل را انتخاب کنید.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: ساخت تگ جدید\n    form:\n      fields:\n        display_name:\n          label: نام\n          msg:\n            empty: نام نمی تواند خالی باشد.\n            range: نام باید نهایتا ۳۵ حرف داشته باشد.\n        slug_name:\n          label: نامک آدس\n          desc: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد.\n          msg:\n            empty: نامک آدرس نمی تواند خالی باشد.\n            range: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد.\n            character: نامک آدرس شامل کلمات غیر مجاز می باشد.\n        desc:\n          label: توضیحات\n        revision:\n          label: تجدید نظر\n        edit_summary:\n          label: ویرایش خلاصه\n          placeholder: >-\n            تغییرات خود را به طور خلاصه توضیح دهید (املا صحیح، دستور زبان مناسب، قالب بندی بهبود یافته)\n    btn_cancel: لغو\n    btn_submit: ثبت\n    btn_post: پست کردن تگ جدید\n  tag_info:\n    created_at: ایجاد شده\n    edited_at: ویرایش شده\n    history: تاریخچه\n    synonyms:\n      title: مترادف ها\n      text: تگ های زیر مجدداً به آنها نگاشت می شوند\n      empty: مترادفی پیدا نشد.\n      btn_add: افزودن مترادف\n      btn_edit: ویرایش\n      btn_save: ذخیره\n    synonyms_text: تگ های زیر مجدداً به آنها نگاشت می شوند\n    delete:\n      title: این برچسب حذف شود\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: مطمئنید که میخواهید حذف شود?\n      close: بستن\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: ویرایش تگ‌\n    default_reason: ویرایش تگ‌\n    default_first_reason: برچسب اضافه کنید\n    btn_save_edits: ذخیره ویرایشها\n    btn_cancel: لغو\n  dates:\n    long_date: ماه ماه ماه روز\n    long_date_with_year: \"ماه روز، سال\"\n    long_date_with_time: \"ماه روز، سال در ساعت:دقیقه\"\n    now: حالا\n    x_seconds_ago: \"{{count}} ثانیه پیش\"\n    x_minutes_ago: \"{{count}} دقیقه پیش\"\n    x_hours_ago: \"{{count}}ساعت پیش\"\n    hour: ساعت\n    day: روز\n    hours: ساعات\n    days: روزها\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: افزودن نظر\n    reply_to: پاسخ به\n    btn_reply: پاسخ\n    btn_edit: ویرایش\n    btn_delete: حذف\n    btn_flag: نشان\n    btn_save_edits: ذخیره ویرایشها\n    btn_cancel: لغو\n    show_more: \"{{count}} نظر بیشتر\"\n    tip_question: >-\n      از نظرات برای درخواست اطلاعات بیشتر یا پیشنهاد بهبود استفاده کنید. از پاسخ دادن به سوالات در نظرات خودداری کنید.\n    tip_answer: >-\n      از نظرات برای پاسخ دادن به سایر کاربران یا اطلاع دادن آنها از تغییرات استفاده کنید. اگر اطلاعات جدیدی اضافه می کنید، به جای نظر دادن، پست خود را ویرایش کنید.\n    tip_vote: چیز مفیدی به پست اضافه می کند\n  edit_answer:\n    title: ویرایش جواب\n    default_reason: ویرایش جواب\n    default_first_reason: پاسخ را اضافه کنید\n    form:\n      fields:\n        revision:\n          label: بازنگری\n        answer:\n          label: پاسخ\n          feedback:\n            characters: متن باید حداقل ۶ حرف داشته باشد.\n        edit_summary:\n          label: ویرایش خلاصه\n          placeholder: >-\n            بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی)\n    btn_save_edits: ذخیره ویرایش ها\n    btn_cancel: لغو\n  tags:\n    title: برچسب ها\n    sort_buttons:\n      popular: محبوب\n      name: نام\n      newest: جدیدترین\n    button_follow: دنبال کردن\n    button_following: دنبال می کنید\n    tag_label: سوالات\n    search_placeholder: فیلتر بر اساس اسم برچسب\n    no_desc: برچسب هیچ توضیحی ندارد.\n    more: بیشتر\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: سوال را ویرایش کنید\n    default_reason: سوال را ویرایش کنید\n    default_first_reason: Create question\n    similar_questions: سؤال های مشابه\n    form:\n      fields:\n        revision:\n          label: تجدید نظر\n        title:\n          label: عنوان\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: عنوان نمی تواند خالی باشد.\n            range: عنوان میتواند تا ۳۰ حرف باشد\n        body:\n          label: بدنه\n          msg:\n            empty: بدنه نمی تواند خالی باشد.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: برچسب ها\n          msg:\n            empty: برچسب ها نمی تواند خالی باشد.\n        answer:\n          label: پاسخ\n          msg:\n            empty: جواب نمی تواند خالی باشد.\n        edit_summary:\n          label: ویرایش خلاصه\n          placeholder: >-\n            بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی)\n    btn_post_question: سوال خود را پست کنید\n    btn_save_edits: ذخیره ویرایش ها\n    answer_question: جواب دادن به سوال خودتان\n    post_question&answer: سوال خود را پست و جواب دهید\n  tag_selector:\n    add_btn: اضافه کردن برچسب\n    create_btn: ایجاد یک برچسب جدید\n    search_tag: جست‌وجوی برچسب‌\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: هیچ تگی مطابقت ندارد\n    tag_required_text: تگ نیاز هست (حداقل یک مورد)\n  header:\n    nav:\n      question: سوالات\n      tag: تگ‌ها\n      user: کاربران\n      badges: Badges\n      profile: پروفایل\n      setting: تنظیمات\n      logout: خروج\n      admin: ادمین\n      review: بازبینی\n      bookmark: نشانک ها\n      moderation: مدیریت\n    search:\n      placeholder: جستجو\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: تغییر\n    loading: درحال بارگذاری...\n  pic_auth_code:\n    title: کپچا\n    placeholder: متن بالا را تاپ کنید\n    msg:\n      empty: کپچا نمی تواند خالی باشد.\n  inactive:\n    first: >-\n      شما تقریباً آماده شده اید! ما یک ایمیل فعال سازی به <bold>{{mail}}</bold> ارسال کردیم. لطفا دستورالعمل های ایمیل را برای فعال کردن حساب خود دنبال کنید.\n    info: \"اگر ایمیل ارسالی را دریافت نکردید، قسمت spam خود را چک کنید.\"\n    another: >-\n      ایمیل فعال‌سازی دیگری را به آدرس <bold>{{mail}}</bold> برای شما ارسال کردیم. ممکن است چند دقیقه طول بکشد تا دستتان برسد. پوشه هرزنامه خود را حتما چک کنید.\n    btn_name: ارسال مجدد کد فعالسازی\n    change_btn_name: تغییر ایمیل\n    msg:\n      empty: نمی‌تواند خالی باشد.\n    resend_email:\n      url_label: آیا مطمئن هستید که می خواهید ایمیل فعال سازی را دوباره ارسال کنید؟\n      url_text: همچنین می توانید لینک فعال سازی بالا را در اختیار کاربر قرار دهید.\n  login:\n    login_to_continue: برای ادامه وارد حساب کاربری شوید\n    info_sign: حساب کاربری ندارید؟ </1>ثبت نام<1>\n    info_login: از قبل حساب کاربری دارید؟ <1>وارد شوید</1>\n    agreements: با ثبت نام، با <1>خط مشی رازداری</1> و <3>شرایط خدمات</3> موافقت می کنید.\n    forgot_pass: رمزعبور را فراموش کردید?\n    name:\n      label: نام\n      msg:\n        empty: نام نمی‌تواند خالی باشد.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: ایمیل\n      msg:\n        empty: ایمیل نمی تواند خالی باشد.\n    password:\n      label: رمز عبور\n      msg:\n        empty: رمز عبور نمی تواند خالی باشد.\n        different: پسوردهای وارد شده در هر دو طرف متناقض هستند\n  account_forgot:\n    page_title: رمزعبور را فراموش کردید\n    btn_name: ارسال ایمیل بازیابی\n    send_success: >-\n      اگر یک حساب با <strong>{{mail}}</strong> مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید.\n    email:\n      label: ایمیل\n      msg:\n        empty: ایمیل نمی تواند خالی باشد.\n  change_email:\n    btn_cancel: لغو\n    btn_update: نشانی ایمیل را به روز کنید\n    send_success: >-\n      اگر یک حساب با <strong>{{mail}}</strong> مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید.\n    email:\n      label: ایمیل جدید\n      msg:\n        empty: ایمیل نمی تواند خالی باشد.\n  oauth:\n    connect: ارتباط با {{ auth_name }}\n    remove: حذف {{ auth_name }}\n  oauth_bind_email:\n    subtitle: یک ایمیل بازیابی به حساب خود اضافه کنید.\n    btn_update: نشانی ایمیل را به روز کنید\n    email:\n      label: ایمیل\n      msg:\n        empty: ایمیل نمی تواند خالی باشد.\n    modal_title: ایمیل تکراری.\n    modal_content: این ایمیل قبلا ثبت نام کرده است. آیا مطمئن هستید که می خواهید به حساب ثبت نام کرده متصل شوید?\n    modal_cancel: تغییر ایمیل\n    modal_confirm: به حساب کاربری ثبت نام کرده متصل شوید\n  password_reset:\n    page_title: بازیابی کلمه عبور\n    btn_name: رمز عبورم را بازنشانی کن\n    reset_success: >-\n      شما با موفقیت رمز عبور خود را تغییر دادید، به صفحه ورود هدایت می شوید.\n    link_invalid: >-\n      متاسفم،‌ لینک بازنشانی رمز عبور دیگر اعتبار ندارد. شاید رمز عبور شما قبلا تغییر کرده است?\n    to_login: ادامه بدهید تا به صفحه ورود برسید\n    password:\n      label: رمز عبور\n      msg:\n        empty: رمز عبور نمی تواند خالی باشد.\n        length: تعداد حروف باید بین ۸ تا ۳۲ باشد\n        different: پسوردهای وارد شده در هر دو طرف متناقض هستند\n    password_confirm:\n      label: تأیید رمز عبور جديد\n  settings:\n    page_title: تنظیمات\n    goto_modify: برای تغییر بروید\n    nav:\n      profile: پروفایل\n      notification: اعلانات\n      account: حساب کاربری\n      interface: رابط کاربری\n    profile:\n      heading: پروفایل\n      btn_name: ذخیره\n      display_name:\n        label: نام\n        msg: نام نمی تواند خالی باشد.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: نام‌کاربری\n        caption: دیگران میتوانند به شما به بصورت \"@username\" اشاره کنند.\n        msg: نام کاربری نمی تواند خالی باشد.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: عکس پروفایل\n        gravatar: Gravatar\n        gravatar_text: می توانید تصویر را تغییر دهید\n        custom: سفارشی\n        custom_text: شما میتوانید عکس خود را بازگذاری کنید.\n        default: سیستم\n        msg: لطفا یک آواتار آپلود کنید\n      bio:\n        label: درباره من\n      website:\n        label: وب سایت\n        placeholder: \"https://example.com\"\n        msg: فرمت نادرست وب سایت\n      location:\n        label: موقعیت\n        placeholder: \"شهر، کشور\"\n    notification:\n      heading: اعلان های ایمیلی\n      turn_on: روشن کردن\n      inbox:\n        label: اعلانات ایمیل\n        description: پاسخ به سوالات خود،‌ نظرات،‌ دعوت ها،‌و بیشتر.\n      all_new_question:\n        label: تمامی سوالات جدید\n        description: درمورد تمامی سوالات جدید با خبر شوید. تا ۵۰ سوال در هفته.\n      all_new_question_for_following_tags:\n        label: تمامی سوالات جدید برای این تگ ها\n        description: درمورد تمامی سوالات جدید در مورد این تگ ها باخبر شوید.\n    account:\n      heading: حساب کاربری\n      change_email_btn: تغییر ایمیل\n      change_pass_btn: تغییر رمز عبور\n      change_email_info: >-\n        ما یک ایمیل به آن آدرس ارسال کردیم. لطفا مراحل تایید را طی کنید.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: رمز عبور فعلی\n        msg: رمز عبور نمی تواند خالی باشد.\n      password_title: رمز عبور\n      current_pass:\n        label: رمز عبور فعلی\n        msg:\n          empty: رمز عبور نمی تواند خالی باشد.\n          length: تعداد حروف باید بین ۸ تا ۳۲ باشد.\n          different: دو رمز عبور وارد شده همخوانی ندارند.\n      new_pass:\n        label: رمز عبور جدید\n      pass_confirm:\n        label: تأیید رمز عبور جديد\n    interface:\n      heading: رابط کاربری\n      lang:\n        label: زبان رابط کاربری\n        text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید.\n    my_logins:\n      title: ورود های من\n      label: با استفاده از این حساب ها وارد این سایت شوید یا ثبت نام کنید.\n      modal_title: حذف ورود\n      modal_content: آیا مطمئن هستید که می خواهید این ورود را از حساب خود حذف کنید؟\n      modal_confirm_btn: حذف\n      remove_success: با موفقیت حذف شد\n  toast:\n    update: بروز رسانی موفق بود\n    update_password: رمز عبور با موفقیت تغییر کرد.\n    flag_success: ممنون بابت اطلاع دادن.\n    forbidden_operate_self: عملیات غیر مجاز\n    review: بازبینی شما پس از بررسی نشان داده خواهد شد.\n    sent_success: با موفقيت ارسال شد\n  related_question:\n    title: Related\n    answers: جواب ها\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: مردم پرسیدند\n    desc: افرادی را دعوت کنید که فکر می کنید ممکن است پاسخ را بدانند.\n    invite: دعوت به پاسخ\n    add: افزودن افراد\n    search: جستجوی افراد\n  question_detail:\n    action: عملیات\n    created: Created\n    Asked: پرسیده شده\n    asked: پرسیده شده\n    update: تغییر یافته\n    Edited: Edited\n    edit: ویرایش شده\n    commented: commented\n    Views: مشاهده شده\n    Follow: دنبال کردن\n    Following: دنبال می کنید\n    follow_tip: برای دریافت اعلان ها این سوال را دنبال کنید\n    answered: جواب داده\n    closed_in: بسته شده د\n    show_exist: نمایش سوال موجود.\n    useful: مفید\n    question_useful: مفید و واضح است\n    question_un_useful: نامشخص یا مفید نیست\n    question_bookmark: این سوال را نشانه گذاری کنید\n    answer_useful: مفید است\n    answer_un_useful: مفید نیست\n    answers:\n      title: پاسخ ها\n      score: امتیاز\n      newest: جدیدترین\n      oldest: Oldest\n      btn_accept: پذیرفتن\n      btn_accepted: پذیرفته شده\n    write_answer:\n      title: پاسخ شما\n      edit_answer: پاسخ فعلی من را ویرایش کنید\n      btn_name: پاسخ خود را ارسال کنید\n      add_another_answer: پاسخ دیگری اضافه کنید\n      confirm_title: به پاسخ دادن ادامه دهید\n      continue: ادامه دهید\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: جواب نمی تواند خالی باشد.\n      characters: متن باید حداقل ۶ حرف داشته باشد.\n      tips:\n        header_1: از جواب شما متشکریم\n        li1_1: لطفا مطمئن شوید که <strong>جواب دهید سوال را</strong>. جزئیات بیشتری ارائه دهید و تحقیقات خود را به اشتراک بگذارید.\n        li1_2: از هر اظهاراتی که می کنید با ارجاعات یا تجربه شخصی پشتیبان بگیرید.\n        header_2: اما <strong>دوری کنید</strong> ...\n        li2_1: درخواست کمک، به دنبال شفاف سازی، یا پاسخ به پاسخ های دیگر.\n    reopen:\n      confirm_btn: بازگشایی مجدد\n      title: بازگشایی مجدد این پست\n      content: آیا مطمئن هستید که می‌خواهید بازگشایی مجدد انجام دهید?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: پست را پین کن\n      content: آیا مطمئن هستید میخواهید پست بصورت عمومی پین شود؟ پست در بالای تمامی پست ها نشان داده خواهد شد.\n      confirm_btn: پین کردن\n  delete:\n    title: حذف این پست\n    question: >-\n      ما <strong>حذف سوالات با جواب</strong> را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.</p><p> حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید?\n    answer_accepted: >-\n      ما <strong>حذف جواب تایید شده</strong> را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.</p><p> حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید?\n    other: مطمئنید که میخواهید حذف شود?\n    tip_answer_deleted: جواب شما حذف شده است\n    undelete_title: حذف این پست\n    undelete_desc: آیا مطمئن به بازگردانی هستید؟\n  btns:\n    confirm: تایید\n    cancel: لغو\n    edit: ویرایش\n    save: ذخیره\n    delete: حذف\n    undelete: بازگردانی\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: ورود\n    signup: عضويت\n    logout: خروج\n    verify: تایید\n    create: Create\n    approve: تایید\n    reject: رد کردن\n    skip: بعدی\n    discard_draft: دور انداختن پیش‌نویس‌\n    pinned: پین شد\n    all: همه\n    question: سوال\n    answer: پاسخ\n    comment: نظر\n    refresh: بارگذاری مجدد\n    resend: ارسال مجدد\n    deactivate: غیرفعال کردن\n    active: فعال\n    suspend: تعلیق\n    unsuspend: لغو تعلیق\n    close: بستن\n    reopen: بازگشایی\n    ok: تأیید\n    light: روشن\n    dark: تیره\n    system_setting: تنظیمات سامانه\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: نتایج جستجو\n    keywords: کلیدواژه ها\n    options: گزینه‌ها\n    follow: دنبال کردن\n    following: دنبال میکنید\n    counts: \"{{count}} نتیجه\"\n    counts_loading: \"... Results\"\n    more: بیشتر\n    sort_btns:\n      relevance: مرتبط\n      newest: جدیدترین\n      active: فعال\n      score: امتیاز\n      more: بیشتر\n    tips:\n      title: گزینه های پیشرفته جستجو\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> جستجو براساس نویسنده\"\n      answer: \"<1>answers:0</1> سوال بی جواب\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: چیزی پیدا نکردیم <br /> کلمات کلیدی متفاوت یا کمتر خاص را امتحان کنید.\n  share:\n    name: اشتراک‌گذاری\n    copy: کپی کردن لینک\n    via: اشتراک گذاری پست با...\n    copied: کپی انجام شد\n    facebook: اشتراک گذاری در فیس بوک\n    twitter: Share to X\n  cannot_vote_for_self: شما نمی توانید به پست خودتان رای دهید.\n  modal_confirm:\n    title: خطا...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: اکانت جدید شما تایید شده است، به صفحه خانه منتقل خواهید شد.\n    link: ادامه بدهید تا به صفحه خانه برسید\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: ایمیل شما به‌روز شده است.\n    confirm_new_email_invalid: >-\n      متاسفیم، لینک تایید دیگر مجاز نیست. شاید حساب شما از قبل فعال شده است?\n  unsubscribe:\n    page_title: قطع عضویت\n    success_title: لغو عضویت موفق (Automatic Translation)\n    success_desc: شما با موفقیت از این لیست مشترک حذف شده اید و دیگر ایمیلی از ما دریافت نخواهید کرد.\n    link: تغییر تنظیمات\n  question:\n    following_tags: تگهای مورد نظر\n    edit: ویرایش\n    save: ذخیره\n    follow_tag_tip: برچسب ها را دنبال کنید تا لیست سوالات خود را تنظیم کنید.\n    hot_questions: سوالات داغ\n    all_questions: تمام سوالات\n    x_questions: \"{{ count }} سوال\"\n    x_answers: \"{{ count }} جواب\"\n    x_posts: \"{{ count }} Posts\"\n    questions: سوالات\n    answers: پاسخ ها\n    newest: جدیدترین\n    active: فعال\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: امتیاز\n    unanswered: بدون پاسخ\n    modified: تغییر یافته\n    answered: جواب داده\n    asked: پرسیده شده\n    closed: بسته\n    follow_a_tag: یک برچسب را دنبال کنید\n    more: بیشتر\n  personal:\n    overview: خلاصه\n    answers: پاسخ ها\n    answer: پاسخ\n    questions: سوالات\n    question: سوال\n    bookmarks: نشان ها\n    reputation: محبوبیت\n    comments: نظرات\n    votes: آراء\n    badges: Badges\n    newest: جدیدترین\n    score: امتیاز\n    edit_profile: ویرایش پروفایل\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: مشاهده شده\n    joined: عضو شد\n    comma: \",\"\n    last_login: مشاهده شده\n    about_me: درباره من\n    about_me_empty: \"// Hello, World !\"\n    top_answers: پاسخ های برتر\n    top_questions: سوالات برتر\n    stats: آمار\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: پذیرفته شده\n    answered: جواب داده\n    asked: پرسیده شده\n    downvoted: رأی منفی\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: محبوبیت\n    x_votes: آرای دریافت شد\n    x_answers: جواب ها\n    x_questions: سوالات\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: بعدی\n    done: انجام شده\n    config_yaml_error: نمیتوان فایل config.yaml را ایجاد کرد.\n    lang:\n      label: لطفا زبان خود را انتخاب کنید\n    db_type:\n      label: موتور پایگاه‌داده\n    db_username:\n      label: نام‌کاربری\n      placeholder: روت\n      msg: نام کاربری نمی تواند خالی باشد.\n    db_password:\n      label: رمز عبور\n      placeholder: روت\n      msg: رمز عبور نمی تواند خالی باشد.\n    db_host:\n      label: میزبان پایگاه داده\n      placeholder: \"db:3306\"\n      msg: پایگاه داده میزبان نمیتواند خالی باشد.\n    db_name:\n      label: نام پایگاه‌داده\n      placeholder: پاسخ\n      msg: نام پایگاه داده میزبان نمیتواند خالی باشد.\n    db_file:\n      label: فایل پایگاه داده\n      placeholder: /data/answer.db\n      msg: فایل پایگاه داده نمیتواند خالی باشد.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Config.yaml را بسازید\n      label: فایل config.yaml ساخته شد.\n      desc: >-\n        شما بصورت دستی میتوانید فایل <1>config.yaml</1> را در پوشه <1>/var/wwww/xxx/ </1> ایجاد و متن را در داخل آن جایگذاری کنید.\n      info: بعد از اتمام،‌ بر روی \"بعدی\" کلیک کنید.\n    site_information: اطلاعات سایت\n    admin_account: حساب ادمین\n    site_name:\n      label: نام سایت\n      msg: نام سایت نمی تواند خالی باشد.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: آدرس سایت\n      text: آدرس سایت شما\n      msg:\n        empty: آدرس سایت نمی تواند خالی باشد.\n        incorrect: فرمت آدرس سایت نادرست است.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: ایمیل ارتباطی\n      text: آدرس ایمیل مخاطب کلیدی پاسخگو برای این سایت.\n      msg:\n        empty: ایمیل مخاطب نمی تواند خالی باشد.\n        incorrect: فرمت ایمیل مخاطب نادرست است.\n    login_required:\n      label: خصوصی\n      switch: ورود الزامی است\n      text: تنها افرادی که وارد سایت شده اند میتوانند به این انجمن دسترسی پیدا کنند.\n    admin_name:\n      label: نام\n      msg: نام نمی‌تواند خالی باشد.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: رمز عبور\n      text: >-\n        شما برای ورود نیازمند این پسورد خواهید بود. لطفا در محل امنی ذخیره کنید.\n      msg: رمز عبور نمی تواند خالی باشد.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: ایمیل\n      text: شما به این ایمیل برای ورود نیاز خواهید داشت.\n      msg:\n        empty: ایمیل نمی تواند خالی باشد.\n        incorrect: فرمت ایمیل نادرست است.\n    ready_title: Your site is ready\n    ready_desc: >-\n      اگر به تغییر بیشتر تنظیمات نیاز دارید،‌به <1> قسمت ادمین </1> مراجعه کنید،‌میتوانید این گزینه را در منو سایت مشاهده کنید.\n    good_luck: \"خوش بگذره و موفق باشید!\"\n    warn_title: هشدار\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: قبلاً نصب شده است\n    installed_desc: >-\n      شما پیش از‌این وردپرس را برپا نموده‌اید. برای راه‌اندازی دوباره ابتدا جدول‌های کهنه در پایگاه‌داده را پاک نمایید.\n    db_failed: اتصال به دیتابیس موفقیت آمیز نبود\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: مشاهده\n    votes: آراء\n    answers: جواب ها\n    accepted: پذیرفته شده\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: شما مجوز دسترسی به این صفحه را ندارید.\n    desc_404: متاسفانه این صفحه وجود ندارد.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: بازگشت به صفحه اصلی\n  page_maintenance:\n    desc: \"ما درحال تعمیر هستیم، به زودی برمی گردیم.\"\n  nav_menus:\n    dashboard: داشبرد\n    contents: محتوا\n    questions: سوالات\n    answers: پاسخ ها\n    users: کاربران\n    badges: Badges\n    flags: پرچم\n    settings: تنظیمات\n    general: عمومی\n    interface: رابط کاربری\n    smtp: SMTP\n    branding: نام تجاری\n    legal: قانونی\n    write: نوشتن\n    terms: Terms\n    tos: قوانین\n    privacy: حریم خصوصی\n    seo: سئو\n    customize: شخصی‌سازی\n    themes: پوسته ها\n    login: ورود\n    privileges: مجوزها\n    plugins: افزونه‌ها\n    installed_plugins: پلاگین های نصب شده\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: به {{site_name}} خوش آمدید\n  user_center:\n    login: ورود\n    qrcode_login_tip: لطفاً از {{ agentName }} برای اسکن کد QR و ورود به سیستم استفاده کنید.\n    login_failed_email_tip: ورود ناموفق بود، لطفاً قبل از امتحان مجدد به این برنامه اجازه دهید به اطلاعات ایمیل شما دسترسی داشته باشد.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: ادمین\n    dashboard:\n      title: داشبرد\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"سوالات:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"جواب ها:\"\n      comments: \"نظرات:\"\n      votes: \"آرا:\"\n      users: \"Users:\"\n      flags: \"علامت‌ها:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"نسخه:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: در دسترس عموم\n      smtp: \"SMTP:\"\n      timezone: \"منطقه زمانی:\"\n      system_info: اطلاعات سامانه\n      go_version: \"نسخه Go:\"\n      database: \"پایگاه داده:\"\n      database_size: \"اندازه پایگاه داده :\"\n      storage_used: \"فضای استفاده شده:\"\n      uptime: \"پایداری:\"\n      links: Links\n      plugins: افزونه‌ها\n      github: GitHub\n      blog: بلاگ\n      contact: تماس\n      forum: Forum\n      documents: اسناد\n      feedback: ﺑﺎﺯﺧﻮﺭﺩ\n      support: پشتیبانی\n      review: بازبینی\n      config: کانفینگ\n      update_to: آپدیت کردن به\n      latest: آخرین\n      check_failed: بررسی ناموفق بود\n      \"yes\": \"بله\"\n      \"no\": \"نه\"\n      not_allowed: غیر مجاز\n      allowed: مجاز\n      enabled: فعال\n      disabled: غیرفعال\n      writable: قابل نوشتن\n      not_writable: غیرقابل کُپی\n    flags:\n      title: علامت‌ها\n      pending: در حالت انتظار\n      completed: تکمیل شده\n      flagged: علامت گذاری شده\n      flagged_type: پرچم گذاری شده {{ type }}\n      created: ایجاد شده\n      action: عمل\n      review: بازبینی\n    user_role_modal:\n      title: تغییر نقش کاربر به ...\n      btn_cancel: لغو\n      btn_submit: ثبت\n    new_password_modal:\n      title: تعیین رمزعبور جدید\n      form:\n        fields:\n          password:\n            label: رمز عبور\n            text: کاربر از سیستم خارج می شود و باید دوباره وارد سیستم شود.\n            msg: رمز عبور باید 8 تا 32 کاراکتر باشد.\n      btn_cancel: لغو\n      btn_submit: ثبت\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: افزودن کاربر جدید\n      form:\n        fields:\n          users:\n            label: اضافه کردن انبوه کاربر\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: '\"نام، ایمیل، رمز عبور\" را با کاما جدا کنید. یک کاربر در هر خط.'\n            msg: \"لطفا ایمیل کاربر را در هر خط یکی وارد کنید.\"\n          display_name:\n            label: نمایش نام\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: ایمیل\n            msg: ایمیل معتبر نمی‌باشد\n          password:\n            label: رمز عبور\n            msg: رمز عبور باید 8 تا 32 کاراکتر باشد.\n      btn_cancel: لغو\n      btn_submit: ثبت\n    users:\n      title: کاربرها\n      name: نام\n      email: ایمیل\n      reputation: محبوبیت\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: وضعیت\n      role: نقش\n      action: عمل\n      change: تغییر\n      all: همه\n      staff: کارکنان\n      more: بیشتر\n      inactive: غیرفعال\n      suspended: تعلیق شده\n      deleted: حذف شده\n      normal: عادي\n      Moderator: مدير\n      Admin: ادمین\n      User: کاربر\n      filter:\n        placeholder: \"فیلتر براساس، user:id\"\n      set_new_password: تعیین رمزعبور جدید\n      edit_profile: Edit profile\n      change_status: تغییر وضعیت\n      change_role: تغییر نقش\n      show_logs: نمایش ورود ها\n      add_user: افزودن کاربر\n      deactivate_user:\n        title: غیر فعال کردن کاربر\n        content: یک کاربر غیرفعال باید ایمیل خود را دوباره تایید کند.\n      delete_user:\n        title: این کاربر حذف شود\n        content: آیا مطمئن هستید که میخواهید این کابر را حذف نمایید؟ این اقدام دائمی است!\n        remove: محتوای آنها را حذف کنید\n        label: تمام سوالات، پاسخ ها، نظرات و غیره را حذف کن.\n        text: اگر می‌خواهید فقط حساب کاربر را حذف کنید، این را بررسی نکنید.\n      suspend_user:\n        title: تعلیق این کاربر\n        content: کاربر تعلیق شده نمی‌تواند وارد شود.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: سوالات\n      unlisted: Unlisted\n      post: پست\n      votes: آراء\n      answers: پاسخ ها\n      created: ایجاد شده\n      status: وضعیت\n      action: عمل\n      change: تغییر\n      pending: Pending\n      filter:\n        placeholder: \"فیلتر براساس، user:id\"\n    answers:\n      page_title: پاسخ ها\n      post: پست\n      votes: آراء\n      created: ایجاد شده\n      status: وضعیت\n      action: اقدام\n      change: تغییر\n      filter:\n        placeholder: \"فیلتر براساس، user:id\"\n    general:\n      page_title: عمومی\n      name:\n        label: نام سایت\n        msg: نام سایت نمی تواند خالی باشد.\n        text: \"نام این سایت همانطور که در تگ عنوان استفاده شده است.\"\n      site_url:\n        label: آدرس سایت\n        msg: آدرس سایت نمی تواند خالی باشد.\n        validate: لطفا یک url معتبر وارد کنید.\n        text: آدرس سایت شما.\n      short_desc:\n        label: نام این سایت مورد استفاده قرار گرفته است\n        msg: توضیحات کوتاه سایت نمی تواند خالی باشد.\n        text: \"شرح کوتاه، همانطور که در تگ عنوان در صفحه اصلی استفاده شده است.\"\n      desc:\n        label: توضیحات سایت\n        msg: توضیحات کوتاه سایت نمی تواند خالی باشد.\n        text: \"همانطور که در تگ توضیحات متا استفاده شده است، این سایت را در یک جمله توصیف کنید.\"\n      contact_email:\n        label: ایمیل ارتباطی\n        msg: ایمیل مخاطب نمی تواند خالی باشد.\n        validate: ایمیل تماس معتبر نیست.\n        text: آدرس ایمیل مخاطب کلیدی مسئول این سایت.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: رابط کاربری\n      language:\n        label: زبان رابط کاربری\n        msg: زبان رابط نمی تواند خالی باشد.\n        text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید.\n      time_zone:\n        label: منطقه زمانی\n        msg: منطقه زمانی نمی تواند خالی باشد.\n        text: شهری را در همان منطقه زمانی خود انتخاب کنید.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: از ایمیل\n        msg: ایمیل نباید خالی باشد.\n        text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند.\n      from_name:\n        label: نام فرستنده\n        msg: ایمیل نباید خالی باشد.\n        text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند.\n      smtp_host:\n        label: ميزبان SMTP\n        msg: میزبان SMTP نمی تواند خالی باشد.\n        text: سرور ایمیل شما.\n      encryption:\n        label: رمزگذاری\n        msg: کلید رمزگذاری نمی تواند خالی باشد.\n        text: برای اکثر سرورها SSL گزینه پیشنهادی است.\n        ssl: SSL\n        tls: TLS\n        none: هیچ‌کدام\n      smtp_port:\n        label: پورت SMTP\n        msg: پورت SMTP باید شماره 1 ~ 65535 باشد.\n        text: پورت سرور ایمیل شما.\n      smtp_username:\n        label: نام کاربری SMTP\n        msg: نام کاربری SMTP نمی تواند خالی باشد.\n      smtp_password:\n        label: رمزعبور SMTP\n        msg: رمز عبور مدیر نمی‌تواند خالی باشد.\n      test_email_recipient:\n        label: گیرندگان ایمیل را آزمایش کنید\n        text: آدرس ایمیلی را ارائه دهید که ارسال های آزمایشی را دریافت می کند.\n        msg: گیرندگان ایمیل آزمایشی نامعتبر است\n      smtp_authentication:\n        label: فعال کردن احراز هویت\n        title: تأیید هویت SMTP\n        msg: احراز هویت SMTP نمی تواند خالی باشد.\n        \"yes\": \"بله\"\n        \"no\": \"نه\"\n    branding:\n      page_title: نام تجاری\n      logo:\n        label: لوگو\n        msg: کد نمی تواند خالی باشد.\n        text: تصویر لوگو در سمت چپ بالای سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 و نسبت تصویر بیشتر از 3:1 استفاده کنید. اگر خالی بماند، متن عنوان سایت نشان داده می شود.\n      mobile_logo:\n        label: لوگوی موبایل\n        text: لوگوی مورد استفاده در نسخه موبایلی سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 استفاده کنید. اگر خالی بماند، تصویر از تنظیمات \"لوگو\" استفاده خواهد شد.\n      square_icon:\n        label: نماد مربعی\n        msg: نماد مربعی نمی تواند خالی باشد.\n        text: تصویر به عنوان پایه نمادهای متادیتا استفاده می شود. در حالت ایده آل باید بزرگتر از 512x512 باشد.\n      favicon:\n        label: نمادک\n        text: فاویکون برای سایت شما. برای اینکه روی CDN به درستی کار کند باید یک png باشد. به 32x32 تغییر اندازه خواهد شد. اگر خالی بماند، از \"نماد مربع\" استفاده می شود.\n    legal:\n      page_title: قانونی\n      terms_of_service:\n        label: شرایط خدمات\n        text: \"می توانید محتوای شرایط خدمات را در اینجا اضافه کنید. اگر قبلاً سندی دارید که در جای دیگری میزبانی شده است، URL کامل را در اینجا ارائه دهید.\"\n      privacy_policy:\n        label: حریم خصوصی\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: بهینه‌سازی عملیات موتورهای جستجو\n      permalink:\n        label: پیوند ثابت\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: دامنه های مجاز ایمیل\n        text: دامنه های ایمیلی که کاربران باید با آنها حساب ثبت کنند. یک دامنه در هر خط. وقتی خالی است نادیده گرفته می شود.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: وضعیت\n      action: اقدام\n      deactivate: Deactivate\n      activate: فعال سازی\n      settings: تنظیمات\n    settings_users:\n      title: کاربران\n      avatar:\n        label: آواتار پیش فرض\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: پست فهرست نشده\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/fi_FI.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/fr_FR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Succès.\n    unknown:\n      other: Erreur inconnue.\n    request_format_error:\n      other: Format de fichier incorrect.\n    unauthorized_error:\n      other: Non autorisé.\n    database_error:\n      other: Erreur du serveur de données.\n    forbidden_error:\n      other: Interdit.\n    duplicate_request_error:\n      other: Soumission en double.\n  action:\n    report:\n      other: Signaler\n    edit:\n      other: Éditer\n    delete:\n      other: Supprimer\n    close:\n      other: Fermer\n    reopen:\n      other: Rouvrir\n    forbidden_error:\n      other: Interdit.\n    pin:\n      other: Épingler\n    hide:\n      other: Délister\n    unpin:\n      other: Désépingler\n    show:\n      other: Liste\n    invite_someone_to_answer:\n      other: Modifier\n    undelete:\n      other: Annuler la suppression\n    merge:\n      other: Fusionner\n  role:\n    name:\n      user:\n        other: Utilisateur\n      admin:\n        other: Administrateur\n      moderator:\n        other: Modérateur\n    description:\n      user:\n        other: Par défaut, sans accès spécial.\n      admin:\n        other: Possède tous les droits pour accéder au site.\n      moderator:\n        other: Possède les accès à tous les messages sauf aux paramètres d'administration.\n  privilege:\n    level_1:\n      description:\n        other: Niveau 1 (moins de réputation requise pour une équipe privée, un groupe)\n    level_2:\n      description:\n        other: Niveau 2 (faible réputation requise pour la communauté des startups)\n    level_3:\n      description:\n        other: Niveau 3 (haute réputation requise pour une communauté mature)\n    level_custom:\n      description:\n        other: Niveau personnalisé\n    rank_question_add_label:\n      other: Poser une question\n    rank_answer_add_label:\n      other: Écrire une réponse\n    rank_comment_add_label:\n      other: Ajouter un commentaire\n    rank_report_add_label:\n      other: Signaler\n    rank_comment_vote_up_label:\n      other: Voter favorablement le commentaire\n    rank_link_url_limit_label:\n      other: Poster plus de 2 liens à la fois\n    rank_question_vote_up_label:\n      other: Voter favorablement la question\n    rank_answer_vote_up_label:\n      other: Voter favorablement la réponse\n    rank_question_vote_down_label:\n      other: Voter contre la question\n    rank_answer_vote_down_label:\n      other: Voter contre la réponse\n    rank_invite_someone_to_answer_label:\n      other: Inviter quelqu'un à répondre\n    rank_tag_add_label:\n      other: Créer une nouvelle étiquette\n    rank_tag_edit_label:\n      other: Modifier la description de la balise (à réviser)\n    rank_question_edit_label:\n      other: Modifier la question des autres (à revoir)\n    rank_answer_edit_label:\n      other: Modifier la réponse d'un autre (à revoir)\n    rank_question_edit_without_review_label:\n      other: Modifier la question d'un autre utilisateur sans révision\n    rank_answer_edit_without_review_label:\n      other: Modifier la réponse d'un autre utilisateur sans révision\n    rank_question_audit_label:\n      other: Vérifier la question\n    rank_answer_audit_label:\n      other: Revoir les modifications de la réponse\n    rank_tag_audit_label:\n      other: Évaluer les modifications des tags\n    rank_tag_edit_without_review_label:\n      other: Modifier la description du tag sans révision\n    rank_tag_synonym_label:\n      other: Gérer les tags synonyme\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Mot de passe\n  pass:\n    other: Mot de passe\n  old_pass:\n    other: Mot de passe actuel\n  original_text:\n    other: Ce post\n  email_or_password_wrong_error:\n    other: L'email et le mot de passe ne correspondent pas.\n  error:\n    common:\n      invalid_url:\n        other: URL invalide.\n      status_invalid:\n        other: Statut invalide.\n    password:\n      space_invalid:\n        other: Le mot de passe ne doit pas comporter d'espaces.\n    admin:\n      cannot_update_their_password:\n        other: Vous ne pouvez pas modifier votre mot de passe.\n      cannot_edit_their_profile:\n        other: Vous ne pouvez pas modifier votre profil.\n      cannot_modify_self_status:\n        other: Vous ne pouvez pas modifier votre statut.\n      email_or_password_wrong:\n        other: L'email et le mot de passe ne correspondent pas.\n    answer:\n      not_found:\n        other: Réponse introuvable.\n      cannot_deleted:\n        other: Pas de permission pour supprimer.\n      cannot_update:\n        other: Pas de permission pour mettre à jour.\n      question_closed_cannot_add:\n        other: Les questions sont fermées et ne peuvent pas être ajoutées.\n      content_cannot_empty:\n        other: La réponse ne peut être vide.\n    comment:\n      edit_without_permission:\n        other: Les commentaires ne sont pas autorisés à être modifiés.\n      not_found:\n        other: Commentaire non trouvé.\n      cannot_edit_after_deadline:\n        other: Le commentaire a été posté il y a trop longtemps pour être modifié.\n      content_cannot_empty:\n        other: Le commentaire ne peut être vide.\n    email:\n      duplicate:\n        other: L'adresse e-mail existe déjà.\n      need_to_be_verified:\n        other: L'adresse e-mail doit être vérifiée.\n      verify_url_expired:\n        other: L'URL de vérification de l'email a expiré, veuillez renvoyer l'email.\n      illegal_email_domain_error:\n        other: L'e-mail n'est pas autorisé à partir de ce domaine de messagerie. Veuillez en utiliser un autre.\n    lang:\n      not_found:\n        other: Fichier de langue non trouvé.\n    object:\n      captcha_verification_failed:\n        other: Le Captcha est incorrect.\n      disallow_follow:\n        other: Vous n’êtes pas autorisé à suivre.\n      disallow_vote:\n        other: Vous n’êtes pas autorisé à voter.\n      disallow_vote_your_self:\n        other: Vous ne pouvez pas voter pour votre propre message.\n      not_found:\n        other: Objet non trouvé.\n      verification_failed:\n        other: La vérification a échoué.\n      email_or_password_incorrect:\n        other: L'e-mail et le mot de passe ne correspondent pas.\n      old_password_verification_failed:\n        other: La vérification de l'ancien mot de passe a échoué\n      new_password_same_as_previous_setting:\n        other: Le nouveau mot de passe est le même que le précédent.\n      already_deleted:\n        other: Ce post a été supprimé.\n    meta:\n      object_not_found:\n        other: Méta objet introuvable\n    question:\n      already_deleted:\n        other: Ce message a été supprimé.\n      under_review:\n        other: Votre message est en attente de révision. Il sera visible une fois approuvé.\n      not_found:\n        other: Question non trouvée.\n      cannot_deleted:\n        other: Pas de permission pour supprimer.\n      cannot_close:\n        other: Pas de permission pour fermer.\n      cannot_update:\n        other: Pas de permission pour mettre à jour.\n      content_cannot_empty:\n        other: Le contenu ne peut pas être vide.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Le rang de réputation ne remplit pas la condition.\n      vote_fail_to_meet_the_condition:\n        other: Merci pour vos commentaires. Vous avez besoin d'au moins {{.Rank}} de réputation pour voter.\n      no_enough_rank_to_operate:\n        other: Vous avez besoin d'au moins {{.Rank}} de réputation pour faire cela.\n    report:\n      handle_failed:\n        other: La gestion du rapport a échoué.\n      not_found:\n        other: Rapport non trouvé.\n    tag:\n      already_exist:\n        other: Le tag existe déjà.\n      not_found:\n        other: Tag non trouvé.\n      recommend_tag_not_found:\n        other: Le tag Recommandé n'existe pas.\n      recommend_tag_enter:\n        other: Veuillez saisir au moins un tag.\n      not_contain_synonym_tags:\n        other: Ne dois pas contenir de tags synonymes.\n      cannot_update:\n        other: Pas de permission pour mettre à jour.\n      is_used_cannot_delete:\n        other: Vous ne pouvez pas supprimer un tag utilisé.\n      cannot_set_synonym_as_itself:\n        other: Vous ne pouvez pas définir le synonyme de la balise actuelle comme elle-même.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Le nom d'expéditeur ne peut pas être une adresse e-mail.\n    theme:\n      not_found:\n        other: Thème non trouvé.\n    revision:\n      review_underway:\n        other: Impossible d'éditer actuellement, il y a une version dans la file d'attente des revues.\n      no_permission:\n        other: Aucune autorisation de réviser.\n    user:\n      external_login_missing_user_id:\n        other: La plateforme tierce ne fournit pas un identifiant d'utilisateur unique, vous ne pouvez donc pas vous connecter, veuillez contacter l'administrateur du site.\n      external_login_unbinding_forbidden:\n        other: Veuillez définir un mot de passe de connexion pour votre compte avant de supprimer ce login.\n      email_or_password_wrong:\n        other:\n          other: L'email et le mot de passe ne correspondent pas.\n      not_found:\n        other: Utilisateur non trouvé.\n      suspended:\n        other: L'utilisateur a été suspendu.\n      username_invalid:\n        other: Le nom d'utilisateur est invalide.\n      username_duplicate:\n        other: Nom d'utilisateur déjà utilisé.\n      set_avatar:\n        other: La configuration de l'avatar a échoué.\n      cannot_update_your_role:\n        other: Vous ne pouvez pas modifier votre rôle.\n      not_allowed_registration:\n        other: Actuellement, le site n'est pas ouvert aux inscriptions.\n      not_allowed_login_via_password:\n        other: Actuellement le site n'est pas autorisé à se connecter par mot de passe.\n      access_denied:\n        other: Accès refusé\n      page_access_denied:\n        other: Vous n'avez pas accès à cette page.\n      add_bulk_users_format_error:\n        other: \"Erreur format {{.Field}} près de '{{.Content}}' à la ligne {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Le nombre d'utilisateurs que vous ajoutez simultanément doit être compris entre 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: La lecture de la configuration a échoué\n    database:\n      connection_failed:\n        other: La connexion à la base de données a échoué\n      create_table_failed:\n        other: La création de la table a échoué\n    install:\n      create_config_failed:\n        other: Impossible de créer le fichier config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Format de fichier non supporté.\n    site_info:\n      config_not_found:\n        other: Configuration du site introuvable.\n    badge:\n      object_not_found:\n        other: Objet badge introuvable\n  reason:\n    spam:\n      name:\n        other: Courrier indésirable\n      desc:\n        other: Ce message est une publicité ou un vandalisme. Il n'est pas utile ou pertinent pour le sujet actuel.\n    rude_or_abusive:\n      name:\n        other: grossier ou abusif\n      desc:\n        other: \"Une personne raisonnable trouverait ce contenu inapproprié pour un discours respectueux.\"\n    a_duplicate:\n      name:\n        other: un doublon\n      desc:\n        other: Cette question a déjà été posée et a déjà une réponse.\n      placeholder:\n        other: Entrez le lien de la question existante\n    not_a_answer:\n      name:\n        other: n'est pas une réponse\n      desc:\n        other: \"Cela a été posté comme une réponse, mais il n'essaie pas de répondre à la question. Il devrait s'agir d'un commentaire, d'une autre question, ou devrait être supprimé totalement.\"\n    no_longer_needed:\n      name:\n        other: ce n’est plus nécessaire\n      desc:\n        other: Ce commentaire est obsolète, conversationnel ou non pertinent pour ce post.\n    something:\n      name:\n        other: quelque chose d'autre\n      desc:\n        other: Ce message nécessite l'attention de l'équipe de modération pour une autre raison non listée ci-dessus.\n      placeholder:\n        other: Faites-nous savoir précisément ce qui vous préoccupe\n    community_specific:\n      name:\n        other: une raison spécifique à la communauté\n      desc:\n        other: Cette question ne répond pas à une directive de la communauté.\n    not_clarity:\n      name:\n        other: nécessite plus de détails ou de clarté\n      desc:\n        other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème.\n    looks_ok:\n      name:\n        other: semble bien\n      desc:\n        other: Ce poste est bon en tant que tel et n'est pas de mauvaise qualité.\n    needs_edit:\n      name:\n        other: a besoin d'être modifié, et je l'ai fait\n      desc:\n        other: Améliorez et corrigez vous-même les problèmes liés à ce message.\n    needs_close:\n      name:\n        other: a besoin de fermer\n      desc:\n        other: Une question fermée ne peut pas être répondue, mais peut-être quand même modifiée, votée et commentée.\n    needs_delete:\n      name:\n        other: a besoin d'être supprimé\n      desc:\n        other: Ce message sera supprimé.\n  question:\n    close:\n      duplicate:\n        name:\n          other: courrier indésirable\n        desc:\n          other: Cette question a déjà été posée auparavant et a déjà une réponse.\n      guideline:\n        name:\n          other: une raison spécifique à la communauté\n        desc:\n          other: Cette question ne répond pas à une directive de la communauté.\n      multiple:\n        name:\n          other: a besoin de détails ou de clarté\n        desc:\n          other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème.\n      other:\n        name:\n          other: quelque chose d'autre\n        desc:\n          other: Ce message nécessite l'attention du personnel pour une autre raison non listée ci-dessus.\n    operation_type:\n      asked:\n        other: demandé\n      answered:\n        other: répondu\n      modified:\n        other: modifié\n    deleted_title:\n      other: Question supprimée\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Étiquettes\n    no_description:\n      other: L'étiquette n'a pas de description.\n  notification:\n    action:\n      update_question:\n        other: question mise à jour\n      answer_the_question:\n        other: question répondue\n      update_answer:\n        other: réponse mise à jour\n      accept_answer:\n        other: réponse acceptée\n      comment_question:\n        other: a commenté la question\n      comment_answer:\n        other: a commenté la réponse\n      reply_to_you:\n        other: vous a répondu\n      mention_you:\n        other: vous a mentionné\n      your_question_is_closed:\n        other: Une réponse a été publiée pour votre question\n      your_question_was_deleted:\n        other: Une réponse a été publiée pour votre question\n      your_answer_was_deleted:\n        other: Votre réponse a bien été supprimée\n      your_comment_was_deleted:\n        other: Votre commentaire a été supprimé\n      up_voted_question:\n        other: question approuvée\n      down_voted_question:\n        other: question défavorisée\n      up_voted_answer:\n        other: voter favorablement la réponse\n      down_voted_answer:\n        other: réponse défavorisée\n      up_voted_comment:\n        other: commentaire approuvé\n      invited_you_to_answer:\n        other: vous invite à répondre\n      earned_badge:\n        other: Vous avez gagné le badge \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirmez votre nouvelle adresse e-mail\"\n      body:\n        other: \"Confirmez votre nouvelle adresse électronique pour {{.SiteName}} en cliquant sur le lien suivant :<br>\\\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\\\n\\\\nSi vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail.<br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} a répondu à votre question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\\\n\\\\n{{.DisplayName}}:<br>\\\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\\\n<a href='{{.AnswerUrl}}'>Voir sur {{.SiteName}}</a><br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.<br><br>\\\\n\\\\n<small><a href='{{.UnsubscribeUrl}}'>Désabonner</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} vous a invité à répondre\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\\\n\\\\n{{.DisplayName}}:<br>\\\\n<blockquote>Je pense que vous pourriez connaître la réponse.</blockquote><br>\\\\n<a href='{{.InviteUrl}}'>Voir sur {{.SiteName}}</a><br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.<br><br>\\\\n\\\\n<small><a href='{{.UnsubscribeUrl}}'>Désabonner</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} a commenté votre message\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\\\n\\\\n{{.DisplayName}}:<br>\\\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\\\n<a href='{{.CommentUrl}}'>Voir sur {{.SiteName}}</a><br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.<br><br>\\\\n\\\\n<small><a href='{{.UnsubscribeUrl}}'>Désabonner</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nouvelle question : {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote : Il s'agit d'un e-mail automatique, merci de ne pas répondre à ce message, votre réponse ne pourra être considérée.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Se désabonner</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Réinitialisation du mot de passe\"\n      body:\n        other: \"Quelqu'un a demandé à réinitialiser votre mot de passe sur {{.SiteName}}.<br><br>\\\\n\\\\nSi ce n'était pas vous, vous pouvez ignorer cet e-mail en toute sécurité.<br><br>\\\\n\\\\nCliquez sur le lien suivant pour choisir un nouveau mot de passe :<br>\\\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\\\n<br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirmez la création de votre compte\"\n      body:\n        other: \"Bienvenue sur {{.SiteName}}!<br><br>\\\\n\\\\nCliquez sur le lien suivant pour confirmer et activer votre nouveau compte :<br>\\\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\\\n\\\\nSi le lien ci-dessus n'est pas cliquable, essayez de le copier et de le coller dans la barre d'adresse de votre navigateur web.<br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Email de test\"\n      body:\n        other: \"Ceci est un e-mail de test.<br><br>\\\\n\\\\n--<br>\\\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.\"\n  action_activity_type:\n    upvote:\n      other: vote positif\n    upvoted:\n      other: voté pour\n    downvote:\n      other: voter contre\n    downvoted:\n      other: voté contre\n    accept:\n      other: accepter\n    accepted:\n      other: accepté\n    edit:\n      other: éditer\n  review:\n    queued_post:\n      other: Post en file d'attente\n    flagged_post:\n      other: Signaler post\n    suggested_post_edit:\n      other: Modifications suggérées\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} et {{ .Count }} de plus...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographe\n        desc:\n          other: Informations sur le <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profil</a>.\n      certified:\n        name:\n          other: Certifié\n        desc:\n          other: Nous avons terminé notre nouveau tutoriel d'utilisation.\n      editor:\n        name:\n          other: Éditeur\n        desc:\n          other: Première modification du post.\n      first_flag:\n        name:\n          other: Premier drapeau\n        desc:\n          other: Premier a signalé un post.\n      first_upvote:\n        name:\n          other: Premier vote positif\n        desc:\n          other: Premier a signalé un post.\n      first_link:\n        name:\n          other: Premier lien\n        desc:\n          other: A ajouté un lien vers un autre message.\n      first_reaction:\n        name:\n          other: Première réaction\n        desc:\n          other: Première réaction au post.\n      first_share:\n        name:\n          other: Premier partage\n        desc:\n          other: Premier post partagé.\n      scholar:\n        name:\n          other: Érudit\n        desc:\n          other: A posé une question et accepté une réponse.\n      commentator:\n        name:\n          other: Commentateur\n        desc:\n          other: Laissez 5 commentaires.\n      new_user_of_the_month:\n        name:\n          other: Nouvel utilisateur du mois\n        desc:\n          other: Contributions en suspens au cours de leur premier mois.\n      read_guidelines:\n        name:\n          other: Lire les lignes de conduite\n        desc:\n          other: Lisez les [lignes directrices de la communauté].\n      reader:\n        name:\n          other: Lecteur\n        desc:\n          other: Lisez toutes les réponses dans un sujet avec plus de 10 réponses.\n      welcome:\n        name:\n          other: Bienvenue\n        desc:\n          other: A reçu un vote positif.\n      nice_share:\n        name:\n          other: Bien partagé\n        desc:\n          other: A partagé un poste avec 25 visiteurs uniques.\n      good_share:\n        name:\n          other: Bon partage\n        desc:\n          other: A partagé un poste avec 300 visiteurs uniques.\n      great_share:\n        name:\n          other: Super Partage\n        desc:\n          other: A partagé un poste avec 1000 visiteurs uniques.\n      out_of_love:\n        name:\n          other: Amoureux\n        desc:\n          other: A donné 50 likes dans une journée.\n      higher_love:\n        name:\n          other: Amour plus grand\n        desc:\n          other: A donné 50 likes dans une journée 5 fois.\n      crazy_in_love:\n        name:\n          other: Fou d'amour\n        desc:\n          other: A recueilli 50 votes positifs par jour 20 fois.\n      promoter:\n        name:\n          other: Promoteur\n        desc:\n          other: Inviter un utilisateur.\n      campaigner:\n        name:\n          other: Propagandiste\n        desc:\n          other: A invité 3 utilisateurs de base.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: A invité 5 membres.\n      thank_you:\n        name:\n          other: Merci\n        desc:\n          other: A 20 postes votés et a donné 10 votes.\n      gives_back:\n        name:\n          other: Redonne\n        desc:\n          other: A 100 postes votés et a donné 100 votes.\n      empathetic:\n        name:\n          other: Empathique\n        desc:\n          other: A 500 postes votés et a donné 1000 votes.\n      enthusiast:\n        name:\n          other: Enthousiaste\n        desc:\n          other: Visite de 10 jours consécutifs.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visite de 100 jours consécutifs.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visite de 365 jours consécutifs.\n      anniversary:\n        name:\n          other: Anniversaire\n        desc:\n          other: Membre actif pour une année, affiché au moins une fois.\n      appreciated:\n        name:\n          other: Apprécié\n        desc:\n          other: A reçu 1 vote positif sur 20 posts.\n      respected:\n        name:\n          other: Respecté\n        desc:\n          other: A reçu 2 vote positif sur 100 posts.\n      admired:\n        name:\n          other: Admirée\n        desc:\n          other: A reçu 5 vote positif sur 300 messages.\n      solved:\n        name:\n          other: Résolu\n        desc:\n          other: Une réponse a été acceptée.\n      guidance_counsellor:\n        name:\n          other: Conseiller d'orientation\n        desc:\n          other: 10 réponses sont acceptées.\n      know_it_all:\n        name:\n          other: Tout-savoir\n        desc:\n          other: 50 réponses sont acceptées.\n      solution_institution:\n        name:\n          other: Institution de solution\n        desc:\n          other: 150 réponses sont acceptées.\n      nice_answer:\n        name:\n          other: Belle réponse\n        desc:\n          other: Réponse a obtenu un score de 10 ou plus.\n      good_answer:\n        name:\n          other: Bonne répone\n        desc:\n          other: Réponse a obtenu un score de 25 ou plus.\n      great_answer:\n        name:\n          other: Super Réponse\n        desc:\n          other: Réponse a obtenu un score de 50 ou plus.\n      nice_question:\n        name:\n          other: Belle Question\n        desc:\n          other: Question a obtenu un score de 10 ou plus.\n      good_question:\n        name:\n          other: Bonne Question\n        desc:\n          other: Question a obtenu un score de 25 ou plus.\n      great_question:\n        name:\n          other: Super Question\n        desc:\n          other: Question a obtenu un score de 50 ou plus.\n      popular_question:\n        name:\n          other: Question Populaire\n        desc:\n          other: Question avec 500 points de vue.\n      notable_question:\n        name:\n          other: Question notable\n        desc:\n          other: Question avec 1,000 points de vue.\n      famous_question:\n        name:\n          other: Question célèbre\n        desc:\n          other: Question avec 5000 points de vue.\n      popular_link:\n        name:\n          other: Lien populaire\n        desc:\n          other: A posté un lien externe avec 50 clics.\n      hot_link:\n        name:\n          other: Lien chaud\n        desc:\n          other: A posté un lien externe avec 300 clics.\n      famous_link:\n        name:\n          other: Célèbre lien\n        desc:\n          other: A posté un lien externe avec 100 clics.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Initialisation complète\n      community:\n        name:\n          other: Communauté\n      posting:\n        name:\n          other: Publication\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Comment mettre en forme\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mentionner un post: <code>#post_id</code></p></li><li><p class=\"mb-2\">Pour faire des liens</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\">mettre des retour entre les paragraphes</p></li><li><p class=\"mb-2\"><em>_italic_</em> or **<strong>gras</strong>**</p></li><li><p class=\"mb-2\">indenter le code par 4 espaces</p></li><li><p class=\"mb-2\">citation en plaçant <code>&gt;</code> au début de la ligne</p></li><li><p class=\"mb-2\">guillemets inversés <code>`comme_ca_`</code></p></li><li><p class=\"mb-2\">créer une banière de code avec les guillemets inversés <code>`</code></p><pre class=\"mb-0\"><code>```<br/>code ici<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Préc\n    next: Suivant\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Étiquette\n    tags: Étiquettes\n    tag_wiki: tag wiki\n    create_tag: Créer un tag\n    edit_tag: Modifier l'étiquette\n    ask_a_question: Create Question\n    edit_question: Modifier la question\n    edit_answer: Modifier la réponse\n    search: Rechercher\n    posts_containing: Messages contenant\n    settings: Paramètres\n    notifications: Notifications\n    login: Se connecter\n    sign_up: S'inscrire\n    account_recovery: Récupération de compte\n    account_activation: Activation du compte\n    confirm_email: Confirmer l'email\n    account_suspended: Compte suspendu\n    admin: Admin\n    change_email: Modifier l'e-mail\n    install: Installation d'Answer\n    upgrade: Mise à jour d'Answer\n    maintenance: Maintenance du site\n    users: Utilisateurs\n    oauth_callback: Traitement\n    http_404: Erreur HTTP 404\n    http_50X: Erreur HTTP 500\n    http_403: Erreur HTTP 403\n    logout: Se déconnecter\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Boîte de réception\n    achievement: Accomplissements\n    new_alerts: Nouvelles notifications\n    all_read: Tout marquer comme lu\n    show_more: Afficher plus\n    someone: Quelqu'un\n    inbox_type:\n      all: Tous\n      posts: Publications\n      invites: Invitations\n      votes: Votes\n    answer: Réponse\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Votre compte a été suspendu\n    until_time: \"Votre compte a été suspendu jusqu'au {{ time }}.\"\n    forever: Cet utilisateur a été suspendu pour toujours.\n    end: Vous ne respectez pas les directives de la communauté.\n    contact_us: Contactez-nous\n  editor:\n    blockquote:\n      text: Bloc de citation\n    bold:\n      text: Gras\n    chart:\n      text: Diagramme\n      flow_chart: Organigramme\n      sequence_diagram: Diagramme de séquence\n      class_diagram: Diagramme de classe\n      state_diagram: Diagramme d'état\n      entity_relationship_diagram: Diagramme entité-association\n      user_defined_diagram: Diagramme défini par l'utilisateur\n      gantt_chart: Diagramme de Gantt\n      pie_chart: Camembert\n    code:\n      text: Exemple de Code\n      add_code: Ajouter un exemple de code\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Le code ne peut pas être vide.\n          language:\n            label: Langage\n            placeholder: Détection automatique\n      btn_cancel: Annuler\n      btn_confirm: Ajouter\n    formula:\n      text: Formule\n      options:\n        inline: Formule en ligne\n        block: Bloc de formule\n    heading:\n      text: Titre\n      options:\n        h1: Titre de niveau 1\n        h2: Titre de niveau 2\n        h3: Titre de niveau 3\n        h4: Titre de niveau 4\n        h5: Titre de niveau 5\n        h6: Titre de niveau 6\n    help:\n      text: Aide\n    hr:\n      text: Ligne horizontale\n    image:\n      text: Image\n      add_image: Ajouter une image\n      tab_image: Téléverser une image\n      form_image:\n        fields:\n          file:\n            label: Fichier image\n            btn: Sélectionner une image\n            msg:\n              empty: Le fichier ne doit pas être vide.\n              only_image: Seules les images sont autorisées.\n              max_size: La taille du fichier ne doit pas dépasser {{size}} Mo.\n          desc:\n            label: Description\n      tab_url: URL de l'image\n      form_url:\n        fields:\n          url:\n            label: URL de l'image\n            msg:\n              empty: L'URL de l'image ne peut pas être vide.\n          name:\n            label: Description\n      btn_cancel: Annuler\n      btn_confirm: Ajouter\n      uploading: Téléversement en cours\n    indent:\n      text: Indentation\n    outdent:\n      text: Désindenter\n    italic:\n      text: Mise en valeur\n    link:\n      text: Hyperlien\n      add_link: Ajouter un lien hypertexte\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: L'URL ne peut pas être vide.\n          name:\n            label: Description\n      btn_cancel: Annuler\n      btn_confirm: Ajouter\n    ordered_list:\n      text: Liste numérotée\n    unordered_list:\n      text: Liste à puces\n    table:\n      text: Tableau\n      heading: Titre\n      cell: Cellule\n    file:\n      text: Joindre des fichiers\n      not_supported: \"Ne prenez pas en charge ce type de fichier. Réessayez avec {{file_type}}.\"\n      max_size: \"La taille du fichier ne doit pas dépasser {{size}} Mo.\"\n  close_modal:\n    title: Je ferme ce post comme...\n    btn_cancel: Annuler\n    btn_submit: Valider\n    remark:\n      empty: Ne peut pas être vide.\n    msg:\n      empty: Veuillez sélectionner une raison.\n  report_modal:\n    flag_title: Je suis en train de signaler ce post comme...\n    close_title: Je ferme ce post comme...\n    review_question_title: Vérifier la question\n    review_answer_title: Vérifier la réponse\n    review_comment_title: Revoir le commentaire\n    btn_cancel: Annuler\n    btn_submit: Envoyer\n    remark:\n      empty: Ne peut pas être vide.\n    msg:\n      empty: Veuillez sélectionner une raison s'il vous plaît.\n      not_a_url: Le format de l'URL est incorrect.\n      url_not_match: L'origine de l'URL ne correspond pas au site web actuel.\n  tag_modal:\n    title: Créer un nouveau tag\n    form:\n      fields:\n        display_name:\n          label: Nom Affiché\n          msg:\n            empty: Le nom d'affichage ne peut être vide.\n            range: Le nom doit contenir moins de 35 caractères.\n        slug_name:\n          label: Limace d'URL\n          desc: Titre de 35 caractères maximum.\n          msg:\n            empty: L'URL ne peut pas être vide.\n            range: Titre de 35 caractères maximum.\n            character: Le slug d'URL contient un jeu de caractères non autorisé.\n        desc:\n          label: Description\n        revision:\n          label: Révision\n        edit_summary:\n          label: Modifier le résumé\n          placeholder: >-\n            Expliquez brièvement vos modifications (orthographe corrigée, grammaire corrigée, mise en forme améliorée)\n    btn_cancel: Annuler\n    btn_submit: Valider\n    btn_post: Publier un nouveau tag\n  tag_info:\n    created_at: Créé\n    edited_at: Modifié\n    history: Historique\n    synonyms:\n      title: Synonymes\n      text: Les tags suivants seront redistribués vers\n      empty: Aucun synonyme trouvé.\n      btn_add: Ajouter un synonyme\n      btn_edit: Modifier\n      btn_save: Enregistrer\n    synonyms_text: Les balises suivantes seront remappées en\n    delete:\n      title: Supprimer cette étiquette\n      tip_with_posts: >-\n        <p>Nous ne permettons pas la <strong>suppression d'un tag avec des posts</strong></p><p>Veuillez d'abord supprimer ce tag des posts.</p>\n      tip_with_synonyms: >-\n        <p>Nous ne permettons pas de <strong>supprimer un tag avec des synonymes</strong>.</p> <p>Veuillez d'abord supprimer les synonymes de ce tag.</p>\n      tip: Êtes-vous sûr de vouloir supprimer ?\n      close: Fermer\n    merge:\n      title: Étiquette de fusion\n      source_tag_title: Étiquette de source\n      source_tag_description: Cette étiquette de source et ses données associées seront réorganisées vers l'étiquette cible.\n      target_tag_title: Étiquette cible\n      target_tag_description: Un synonyme entre ces deux étiquettes sera créé après la fusion.\n      no_results: Aucune étiquette correspondante\n      btn_submit: Valider\n      btn_close: Fermer\n  edit_tag:\n    title: Editer le tag\n    default_reason: Éditer le tag\n    default_first_reason: Ajouter un tag\n    btn_save_edits: Enregistrer les modifications\n    btn_cancel: Annuler\n  dates:\n    long_date: D MMM\n    long_date_with_year: \"D MMMM YYYY\"\n    long_date_with_time: \"D MMM YYYY [at] HH:mm\"\n    now: maintenant\n    x_seconds_ago: \"il y a {{count}}s\"\n    x_minutes_ago: \"il y a {{count}}m\"\n    x_hours_ago: \"il y a {{count}}h\"\n    hour: heure\n    day: jour\n    hours: heures\n    days: jours\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: cœur\n    smile: sourire\n    frown: froncer les sourcils\n    btn_label: ajout et suppression de réactions\n    undo_emoji: annuler la réaction {{ emoji }}\n    react_emoji: réagir à {{ emoji }}\n    unreact_emoji: annuler la réaction avec {{ emoji }}\n  comment:\n    btn_add_comment: Ajoutez un commentaire\n    reply_to: Répondre à\n    btn_reply: Répondre\n    btn_edit: Éditer\n    btn_delete: Supprimer\n    btn_flag: Balise\n    btn_save_edits: Enregistrer les modifications\n    btn_cancel: Annuler\n    show_more: \"{{count}} commentaires restants\"\n    tip_question: >-\n      Utilisez les commentaires pour demander plus d'informations ou suggérer des améliorations. Évitez de répondre aux questions dans les commentaires.\n    tip_answer: >-\n      Utilisez des commentaires pour répondre à d'autres utilisateurs ou leur signaler des modifications. Si vous ajoutez de nouvelles informations, modifiez votre message au lieu de commenter.\n    tip_vote: Il ajoute quelque chose d'utile au post\n  edit_answer:\n    title: Modifier la réponse\n    default_reason: Modifier la réponse\n    default_first_reason: Ajouter une réponse\n    form:\n      fields:\n        revision:\n          label: Modification\n        answer:\n          label: Réponse\n          feedback:\n            characters: le contenu doit comporter au moins 6 caractères.\n        edit_summary:\n          label: Modifier le résumé\n          placeholder: >-\n            Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée)\n    btn_save_edits: Enregistrer les modifications\n    btn_cancel: Annuler\n  tags:\n    title: Étiquettes\n    sort_buttons:\n      popular: Populaire\n      name: Nom\n      newest: Le plus récent\n    button_follow: Suivre\n    button_following: Abonnements\n    tag_label: questions\n    search_placeholder: Filtrer par étiquette\n    no_desc: L'étiquette n'a pas de description.\n    more: Plus\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Modifier la question\n    default_reason: Modifier la question\n    default_first_reason: Create question\n    similar_questions: Questions similaires\n    form:\n      fields:\n        revision:\n          label: Modification\n        title:\n          label: Titre\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Le titre ne peut pas être vide.\n            range: Titre de 150 caractères maximum\n        body:\n          label: Corps\n          msg:\n            empty: Le corps ne peut pas être vide.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Étiquettes\n          msg:\n            empty: Les étiquettes ne peuvent pas être vides.\n        answer:\n          label: Réponse\n          msg:\n            empty: La réponse ne peut être vide.\n        edit_summary:\n          label: Modifier le résumé\n          placeholder: >-\n            Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée)\n    btn_post_question: Publier votre question\n    btn_save_edits: Enregistrer les modifications\n    answer_question: Répondre à votre propre question\n    post_question&answer: Publiez votre question et votre réponse\n  tag_selector:\n    add_btn: Ajouter une étiquette\n    create_btn: Créer une nouvelle étiquette\n    search_tag: Rechercher une étiquette\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Aucune étiquette correspondante\n    tag_required_text: Étiquette requise (au moins une)\n  header:\n    nav:\n      question: Questions\n      tag: Étiquettes\n      user: Utilisateurs\n      badges: Badges\n      profile: Profil\n      setting: Paramètres\n      logout: Se déconnecter\n      admin: Administration\n      review: Vérifier\n      bookmark: Favoris\n      moderation: Modération\n    search:\n      placeholder: Rechercher\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Remplacer\n    loading: chargement en cours...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Saisissez le texte ci-dessus\n    msg:\n      empty: Le captcha ne peut pas être vide.\n  inactive:\n    first: >-\n      Vous avez presque fini ! Un mail de confirmation a été envoyé à <bold>{{mail}}</bold>. Veuillez suivre les instructions dans le mail pour activer votre compte.\n    info: \"S'il n'arrive pas, vérifiez dans votre dossier spam.\"\n    another: >-\n      Nous vous avons envoyé un autre e-mail d'activation à <bold>{{mail}}</bold>. Cela peut prendre quelques minutes pour arriver ; assurez-vous de vérifier votre dossier spam.\n    btn_name: Renvoyer le mail d'activation\n    change_btn_name: Modifier l'e-mail\n    msg:\n      empty: Ne peut pas être vide.\n    resend_email:\n      url_label: Êtes-vous sûr de vouloir renvoyer l'email d'activation ?\n      url_text: Vous pouvez également donner le lien d'activation ci-dessus à l'utilisateur.\n  login:\n    login_to_continue: Connectez-vous pour continuer\n    info_sign: Vous n'avez pas de compte ? <1>Inscrivez-vous</1>\n    info_login: Vous avez déjà un compte ? <1>Connectez-vous</1>\n    agreements: En vous inscrivant, vous acceptez la <1>politique de confidentialité</1> et les <3>conditions de service</3>.\n    forgot_pass: Mot de passe oublié ?\n    name:\n      label: Nom\n      msg:\n        empty: Le nom ne peut pas être vide.\n        range: Le nom doit contenir entre 2 et 30 caractères.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: L'email ne peut pas être vide.\n    password:\n      label: Mot de passe\n      msg:\n        empty: Le mot de passe ne peut pas être vide.\n        different: Les mots de passe saisis ne sont pas identiques\n  account_forgot:\n    page_title: Mot de passe oublié\n    btn_name: Envoyer un e-mail de récupération\n    send_success: >-\n      Si un compte est associé à <strong>{{mail}}</strong>, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe.\n    email:\n      label: E-mail\n      msg:\n        empty: L'e-mail ne peut pas être vide.\n  change_email:\n    btn_cancel: Annuler\n    btn_update: Mettre à jour l'adresse e-mail\n    send_success: >-\n      Si un compte est associé à <strong>{{mail}}</strong>, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe.\n    email:\n      label: Nouvel e-mail\n      msg:\n        empty: L'email ne peut pas être vide.\n  oauth:\n    connect: Se connecter avec {{ auth_name }}\n    remove: Retirer {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Ajoutez un e-mail de récupération à votre compte.\n    btn_update: Mettre à jour l'adresse e-mail\n    email:\n      label: Email\n      msg:\n        empty: L'email ne peut pas être vide.\n    modal_title: L'email existe déjà.\n    modal_content: Cette adresse e-mail est déjà enregistrée. Êtes-vous sûr de vouloir vous connecter au compte existant ?\n    modal_cancel: Modifier l'adresse e-mail\n    modal_confirm: Se connecter au compte existant\n  password_reset:\n    page_title: Réinitialiser le mot de passe\n    btn_name: Réinitialiser mon mot de passe\n    reset_success: >-\n      Vous avez modifié votre mot de passe avec succès ; vous allez être redirigé vers la page de connexion.\n    link_invalid: >-\n      Désolé, ce lien de réinitialisation de mot de passe n'est plus valide. Peut-être que votre mot de passe est déjà réinitialisé ?\n    to_login: Continuer vers la page de connexion\n    password:\n      label: Mot de passe\n      msg:\n        empty: Le mot de passe ne peut pas être vide.\n        length: La longueur doit être comprise entre 8 et 32\n        different: Les mots de passe saisis ne sont pas identiques\n    password_confirm:\n      label: Confirmer le nouveau mot de passe\n  settings:\n    page_title: Paramètres\n    goto_modify: Aller modifier\n    nav:\n      profile: Profil\n      notification: Notifications\n      account: Compte\n      interface: Interface\n    profile:\n      heading: Profil\n      btn_name: Enregistrer\n      display_name:\n        label: Nom affiché\n        msg: Le nom ne peut être vide.\n        msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères.\n      username:\n        label: Nom d'utilisateur\n        caption: Les gens peuvent vous mentionner avec \"@username\".\n        msg: Le nom d'utilisateur ne peut pas être vide.\n        msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Photo de profil\n        gravatar: Gravatar\n        gravatar_text: Vous pouvez modifier l'image sur\n        custom: Personnaliser\n        custom_text: Vous pouvez charger votre image.\n        default: Système\n        msg: Veuillez charger un avatar\n      bio:\n        label: Biographie\n      website:\n        label: Site Web\n        placeholder: \"https://example.com\"\n        msg: Format du site web incorrect\n      location:\n        label: Position\n        placeholder: \"Ville, Pays\"\n    notification:\n      heading: Notifications\n      turn_on: Activer\n      inbox:\n        label: Notifications par e-mail\n        description: Réponses à vos questions, commentaires, invitaitons et plus.\n      all_new_question:\n        label: Toutes les nouvelles questions\n        description: Recevez une notification pour toutes les nouvelles questions. Jusqu'à 50 questions par semaine.\n      all_new_question_for_following_tags:\n        label: Toutes les nouvelles questions pour les tags suivants\n        description: Recevez une notification pour toutes les nouvelles questions avec les tags suivants.\n    account:\n      heading: Compte\n      change_email_btn: Modifier l'adresse e-mail\n      change_pass_btn: Changer le mot de passe\n      change_email_info: >-\n        Nous vous avons envoyé un mail à cette adresse. Merci de suivre les instructions.\n      email:\n        label: Email\n      new_email:\n        label: Nouvel e-mail\n        msg: La nouvelle adresse e-mail ne peut pas être vide.\n      pass:\n        label: Mot de passe actuel\n        msg: Le mot de passe ne peut pas être vide.\n      password_title: Mot de passe\n      current_pass:\n        label: Mot de passe actuel\n        msg:\n          empty: Le mot de passe actuel ne peut pas être vide.\n          length: La longueur doit être comprise entre 8 et 32.\n          different: Le mot de passe saisi ne correspond pas.\n      new_pass:\n        label: Nouveau mot de passe\n      pass_confirm:\n        label: Confirmer le nouveau mot de passe\n    interface:\n      heading: Interface\n      lang:\n        label: Langue de l'interface\n        text: Langue de l'interface utilisateur. Cela changera lorsque vous rafraîchissez la page.\n    my_logins:\n      title: Mes identifiants\n      label: Connectez-vous ou inscrivez-vous sur ce site en utilisant ces comptes.\n      modal_title: Supprimer la connexion\n      modal_content: Confirmez-vous vouloir supprimer cette connexion de votre compte ?\n      modal_confirm_btn: Supprimer\n      remove_success: Supprimé avec succès\n  toast:\n    update: mise à jour effectuée\n    update_password: Mot de passe changé avec succès.\n    flag_success: Merci pour votre signalement.\n    forbidden_operate_self: Interdit d'opérer sur vous-même\n    review: Votre révision s'affichera après vérification.\n    sent_success: Envoyé avec succès\n  related_question:\n    title: Related\n    answers: réponses\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Personnes interrogées\n    desc: Invite people who you think might know the answer.\n    invite: Inviter à répondre\n    add: Ajouter des personnes\n    search: Rechercher des personnes\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Demandé\n    asked: demandé\n    update: Modifié\n    Edited: Edited\n    edit: modifié\n    commented: commenté\n    Views: Consultée\n    Follow: S’abonner\n    Following: Abonné(s)\n    follow_tip: Suivre cette question pour recevoir des notifications\n    answered: répondu\n    closed_in: Fermé dans\n    show_exist: Afficher la question existante.\n    useful: Utile\n    question_useful: C'est utile et clair\n    question_un_useful: Ce n'est pas clair ou n'est pas utile\n    question_bookmark: Ajouter cette question à vos favoris\n    answer_useful: C'est utile\n    answer_un_useful: Ce n'est pas utile\n    answers:\n      title: Réponses\n      score: Score\n      newest: Les plus récents\n      oldest: Le plus ancien\n      btn_accept: Accepter\n      btn_accepted: Accepté\n    write_answer:\n      title: Votre réponse\n      edit_answer: Modifier ma réponse existante\n      btn_name: Poster votre réponse\n      add_another_answer: Ajouter une autre réponse\n      confirm_title: Continuer à répondre\n      continue: Continuer\n      confirm_info: >-\n        <p>Êtes-vous sûr de vouloir ajouter une autre réponse ?</p><p>Vous pouvez utiliser le lien d'édition pour affiner et améliorer votre réponse existante.</p>\n      empty: La réponse ne peut être vide.\n      characters: le contenu doit comporter au moins 6 caractères.\n      tips:\n        header_1: Merci pour votre réponse\n        li1_1: N’oubliez pas de <strong>répondre à la question</strong>. Fournissez des détails et partagez vos recherches.\n        li1_2: Sauvegardez toutes les déclarations que vous faites avec des références ou une expérience personnelle.\n        header_2: Mais <strong>évitez</strong>...\n        li2_1: Demander de l'aide, chercher des éclaircissements ou répondre à d'autres réponses.\n    reopen:\n      confirm_btn: Rouvrir\n      title: Rouvrir ce message\n      content: Êtes-vous sûr de vouloir rouvrir ?\n    list:\n      confirm_btn: Liste\n      title: Lister ce message\n      content: Êtes-vous sûr de vouloir lister ?\n    unlist:\n      confirm_btn: Délister\n      title: Masquer ce message de la liste\n      content: Êtes-vous sûr de vouloir masquer ce message de la liste ?\n    pin:\n      title: Épingler cet article\n      content: Êtes-vous sûr de vouloir l'épingler globalement ? Ce message apparaîtra en haut de toutes les listes de messages.\n      confirm_btn: Épingler\n  delete:\n    title: Supprimer la publication\n    question: >-\n      Nous ne recommandons pas <strong>de supprimer des questions avec des réponses</strong> car cela prive les futurs lecteurs de cette connaissance.</p><p>Suppression répétée des questions répondues peut empêcher votre compte de poser. Êtes-vous sûr de vouloir supprimer ?\n    answer_accepted: >-\n      <p>Nous ne recommandons pas <strong>de supprimer la réponse acceptée</strong> car cela prive les futurs lecteurs de cette connaissance. </p> La suppression répétée des réponses acceptées peut empêcher votre compte de répondre. Êtes-vous sûr de vouloir supprimer ?\n    other: Êtes-vous sûr de vouloir supprimer ?\n    tip_answer_deleted: Cette réponse a été supprimée\n    undelete_title: Annuler la suppression de ce message\n    undelete_desc: Êtes-vous sûr de vouloir annuler la suppression ?\n  btns:\n    confirm: Confimer\n    cancel: Annuler\n    edit: Modifier\n    save: Enregistrer\n    delete: Supprimer\n    undelete: Annuler la suppression\n    list: Liste\n    unlist: Délister\n    unlisted: Non listé\n    login: Se connecter\n    signup: S'inscrire\n    logout: Se déconnecter\n    verify: Vérifier\n    create: Créer\n    approve: Approuver\n    reject: Rejeter\n    skip: Ignorer\n    discard_draft: Abandonner le brouillon\n    pinned: Épinglé\n    all: Tous\n    question: Question\n    answer: Réponse\n    comment: Commentaire\n    refresh: Actualiser\n    resend: Renvoyer\n    deactivate: Désactiver\n    active: Actif\n    suspend: Suspendre\n    unsuspend: Lever la suspension\n    close: Fermer\n    reopen: Rouvrir\n    ok: OK\n    light: Clair\n    dark: Sombre\n    system_setting: Paramètres système\n    default: Défaut\n    reset: Réinitialiser\n    tag: Étiquette\n    post_lowercase: publier\n    filter: Filtre\n    ignore: Ignore\n    submit: Soumettre\n    normal: Normal\n    closed: Fermé\n    deleted: Supprimé\n    deleted_permanently: Supprimé définitivement\n    pending: En attente de traitement\n    more: Plus\n    view: Vue\n    card: Carte\n    compact: Compact\n    display_below: Afficher dessous\n    always_display: Toujours afficher\n    or: ou\n    back_sites: Retour aux sites\n  search:\n    title: Résultats de la recherche\n    keywords: Mots-clés\n    options: Options\n    follow: Suivre\n    following: Abonnements\n    counts: \"{{count}} Résultats\"\n    counts_loading: \"... Results\"\n    more: Plus\n    sort_btns:\n      relevance: Pertinence\n      newest: Les plus récents\n      active: Actif\n      score: Score\n      more: Plus\n    tips:\n      title: Astuces de recherche avancée\n      tag: \"<1>[tag]</1> recherche à l'aide d'un tag\"\n      user: \"<1>utilisateur:username</1> recherche par auteur\"\n      answer: \"<1>réponses:0</1> questions sans réponses\"\n      score: \"<1>score:3</1> messages avec plus de 3 points\"\n      question: \"<1>est:question</1> rechercher des questions\"\n      is_answer: \"<1>est :réponse</1> réponses de recherche\"\n    empty: Nous n'avons rien trouvé. <br /> Essayez des mots-clés différents ou moins spécifiques.\n  share:\n    name: Partager\n    copy: Copier le lien\n    via: Partager via...\n    copied: Copié\n    facebook: Partager sur Facebook\n    twitter: Partager sur X\n  cannot_vote_for_self: Vous ne pouvez pas voter pour votre propre message.\n  modal_confirm:\n    title: Erreur...\n  delete_permanently:\n    title: Supprimer définitivement\n    content: Êtes-vous sûr de vouloir supprimer définitivement ?\n  account_result:\n    success: Votre nouveau compte est confirmé; vous serez redirigé vers la page d'accueil.\n    link: Continuer vers la page d'accueil\n    oops: Oups !\n    invalid: Le lien que vous utilisez ne fonctionne plus.\n    confirm_new_email: Votre adresse email a été mise à jour.\n    confirm_new_email_invalid: >-\n      Désolé, ce lien de confirmation n'est plus valide. Votre email est peut-être déjà modifié ?\n  unsubscribe:\n    page_title: Se désabonner\n    success_title: Désabonnement réussi\n    success_desc: Vous avez été supprimé de cette liste d'abonnés avec succès et ne recevrez plus d'e-mails.\n    link: Modifier les paramètres\n  question:\n    following_tags: Hashtags suivis\n    edit: Éditer\n    save: Enregistrer\n    follow_tag_tip: Suivez les tags pour organiser votre liste de questions.\n    hot_questions: Questions populaires\n    all_questions: Toutes les questions\n    x_questions: \"{{ count }} questions\"\n    x_answers: \"{{ count }} réponses\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Réponses\n    newest: Les plus récents\n    active: Actif\n    hot: Populaires\n    frequent: Fréquent\n    recommend: Recommandé\n    score: Score\n    unanswered: Sans réponse\n    modified: modifié\n    answered: répondu\n    asked: demandé\n    closed: fermé\n    follow_a_tag: Suivre ce tag\n    more: Plus\n  personal:\n    overview: Aperçu\n    answers: Réponses\n    answer: réponse\n    questions: Questions\n    question: question\n    bookmarks: Favoris\n    reputation: Réputation\n    comments: Commentaires\n    votes: Votes\n    badges: Badges\n    newest: Les plus récents\n    score: Score\n    edit_profile: Éditer le profil\n    visited_x_days: \"Visité {{ count }} jours\"\n    viewed: Vu\n    joined: Inscrit\n    comma: \",\"\n    last_login: Vu\n    about_me: À propos de moi\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Les meilleures réponses\n    top_questions: Questions les plus populaires\n    stats: Statistiques\n    list_empty: Aucune publication trouvée.<br />Peut-être souhaiteriez-vous sélectionner un autre onglet ?\n    content_empty: Aucun post trouvé.\n    accepted: Accepté\n    answered: a répondu\n    asked: a demandé\n    downvoted: voté contre\n    mod_short: MOD\n    mod_long: Modérateurs\n    x_reputation: réputation\n    x_votes: votes reçus\n    x_answers: réponses\n    x_questions: questions\n    recent_badges: Badges récents\n  install:\n    title: Installation\n    next: Suivant\n    done: Terminé\n    config_yaml_error: Impossible de créer le fichier config.yaml.\n    lang:\n      label: Veuillez choisir une langue\n    db_type:\n      label: Moteur de base de données\n    db_username:\n      label: Nom d'utilisateur\n      placeholder: root\n      msg: Le nom d'utilisateur ne peut pas être vide.\n    db_password:\n      label: Mot de passe\n      placeholder: root\n      msg: Le mot de passe ne peut pas être vide.\n    db_host:\n      label: Hôte de la base de données\n      placeholder: \"db:3306\"\n      msg: L'hôte de la base de données ne peut pas être vide.\n    db_name:\n      label: Nom de la base de données\n      placeholder: réponse\n      msg: Le nom de la base de données ne peut pas être vide.\n    db_file:\n      label: Fichier de base de données\n      placeholder: /data/answer.db\n      msg: Le fichier de base de données ne doit pas être vide.\n    ssl_enabled:\n      label: Activer SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: Mode SSL\n    ssl_root_cert:\n      placeholder: Chemin du fichier sslrootcert\n      msg: Le chemin vers le fichier sslrootcert ne peut pas être vide\n    ssl_cert:\n      placeholder: Chemin du fichier sslcert\n      msg: Le chemin vers le fichier sslcert ne peut pas être vide\n    ssl_key:\n      placeholder: Chemin du fichier sslkey\n      msg: Le chemin vers le fichier sslkey ne peut pas être vide\n    config_yaml:\n      title: Créer config.yaml\n      label: Le fichier config.yaml a été créé.\n      desc: >-\n        Vous pouvez créer manuellement le fichier <1>config.yaml</1> dans le répertoire <1>/var/wwww/xxx/</1> et y coller le texte suivant.\n      info: Après avoir fini, cliquez sur le bouton \"Suivant\".\n    site_information: Informations du site\n    admin_account: Compte Admin\n    site_name:\n      label: Nom du site\n      msg: Le nom ne peut pas être vide.\n      msg_max_length: Le nom affiché doit avoir une longueur de 4 à 30 caractères.\n    site_url:\n      label: URL du site\n      text: L'adresse de ce site.\n      msg:\n        empty: L'URL ne peut pas être vide.\n        incorrect: Le format de l'URL est incorrect.\n        max_length: L'URL du site doit avoir une longueur maximale de 512 caractères.\n    contact_email:\n      label: Email de contact\n      text: L'adresse email du responsable du site.\n      msg:\n        empty: L'email de contact ne peut pas être vide.\n        incorrect: Le format de l'email du contact est incorrect.\n    login_required:\n      label: Privé\n      switch: Connexion requise\n      text: Seuls les utilisateurs connectés peuvent accéder à cette communauté.\n    admin_name:\n      label: Nom\n      msg: Le nom ne peut pas être vide.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: La longueur du nom doit être comprise entre 2 et 30 caractères.\n    admin_password:\n      label: Mot de passe\n      text: >-\n        Vous aurez besoin de ce mot de passe pour vous connecter . Sauvegarder le de façon sécurisée.\n      msg: Le mot de passe ne peut pas être vide.\n      msg_min_length: Le mot de passe doit comporter au moins 8 caractères.\n      msg_max_length: Le mot de passe doit comporter au maximum 32 caractères.\n    admin_confirm_password:\n      label: \"Répétez le mot de passe\"\n      text: \"Veuillez saisir à nouveau votre mot de passe pour confirmer.\"\n      msg: \"Les mots de passe ne correspondent pas.\"\n    admin_email:\n      label: Email\n      text: Vous aurez besoin de cet email pour vous connecter.\n      msg:\n        empty: L'email ne peut pas être vide.\n        incorrect: Le format de l'email est incorrect.\n    ready_title: Votre site est prêt\n    ready_desc: >-\n      Si vous avez envie de changer plus de paramètres, visitez la <1>section admin</1>; retrouvez la dans le menu du site.\n    good_luck: \"Amusez-vous et bonne chance !\"\n    warn_title: Attention\n    warn_desc: >-\n      Le fichier <1>config.yaml</1> existe déjà. Si vous avez besoin de réinitialiser l'un des éléments de configuration de ce fichier, veuillez le supprimer d'abord.\n    install_now: Vous pouvez essayer de <1>l'installer maintenant</1>.\n    installed: Déjà installé\n    installed_desc: >-\n      Il semble que se soit déjà installer. Pour tout réinstaller, veuillez d'abord nettoyer votre ancienne base de données.\n    db_failed: La connexion à la base de données a échoué\n    db_failed_desc: >-\n      Cela signifie que les informations de la base de données dans votre fichier <1>config.yaml</1> est incorrect ou le contact avec le serveur de base de données n'a pas pu être établi. Cela pourrait signifier que le serveur de base de données de votre hôte est hors service.\n  counts:\n    views: vues\n    votes: votes\n    answers: réponses\n    accepted: Accepté\n  page_error:\n    http_error: Erreur HTTP {{ code }}\n    desc_403: Vous n'avez pas l'autorisation d'accéder à cette page.\n    desc_404: Malheureusement, cette page n'existe pas.\n    desc_50X: Le serveur a rencontré une erreur et n'a pas pu répondre à votre requête.\n    back_home: Retour à la page d'accueil\n  page_maintenance:\n    desc: \"Nous sommes en maintenance, nous serons bientôt de retour.\"\n  nav_menus:\n    dashboard: Tableau de bord\n    contents: Contenus\n    questions: Questions\n    answers: Réponses\n    users: Utilisateurs\n    badges: Badges\n    flags: Signalements\n    settings: Paramètres\n    general: Général\n    interface: Interface\n    smtp: SMTP\n    branding: Marque\n    legal: Légal\n    write: Écrire\n    terms: Terms\n    tos: Conditions d'utilisation\n    privacy: Confidentialité\n    seo: SEO\n    customize: Personnaliser\n    themes: Thèmes\n    login: Se connecter\n    privileges: Privilèges\n    plugins: Extensions\n    installed_plugins: Extensions installées\n    apperance: Apparence\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Bienvenue sur {{site_name}}\n  user_center:\n    login: Connexion\n    qrcode_login_tip: Veuillez utiliser {{ agentName }} pour scanner le code QR et vous connecter.\n    login_failed_email_tip: La connexion a échoué, veuillez autoriser cette application à accéder à vos informations de messagerie avant de réessayer.\n  badges:\n    modal:\n      title: Félicitations\n      content: Vous avez gagné un nouveau badge.\n      close: Fermer\n      confirm: Voir les badges\n    title: Badges\n    awarded: Octroyé\n    earned_×: Gagné ×{{ number }}\n    ×_awarded: \"{{ number }} octroyés\"\n    can_earn_multiple: Vous pouvez gagner cela plusieurs fois.\n    earned: Gagné\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Tableau de bord\n      welcome: Bienvenue dans l'admin !\n      site_statistics: Statistiques du site\n      questions: \"Questions :\"\n      resolved: \"Résolu :\"\n      unanswered: \"Sans réponse :\"\n      answers: \"Réponses :\"\n      comments: \"Commentaires:\"\n      votes: \"Votes :\"\n      users: \"Utilisateurs :\"\n      flags: \"Signalements:\"\n      reviews: \"Revoir :\"\n      site_health: Etat du site\n      version: \"Version :\"\n      https: \"HTTPS :\"\n      upload_folder: \"Dossier de téléversement :\"\n      run_mode: \"Mode de fonctionnement :\"\n      private: Privé\n      public: Public\n      smtp: \"SMTP :\"\n      timezone: \"Fuseau horaire :\"\n      system_info: Informations système\n      go_version: \"Version de Go :\"\n      database: \"Base de donnée :\"\n      database_size: \"Taille de la base de données :\"\n      storage_used: \"Stockage utilisé :\"\n      uptime: \"Uptime :\"\n      links: Liens\n      plugins: Extensions\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Commentaires\n      support: Support\n      review: Vérifier\n      config: Configuration\n      update_to: Mise à jour vers\n      latest: Récents\n      check_failed: Vérification échouée\n      \"yes\": \"Oui\"\n      \"no\": \"Non\"\n      not_allowed: Non autorisé\n      allowed: Autorisé\n      enabled: Activé\n      disabled: Désactivé\n      writable: Écriture autorisée\n      not_writable: Écriture refusée\n    flags:\n      title: Signalements\n      pending: En attente\n      completed: Complété\n      flagged: Signalé\n      flagged_type: Signalé {{ type }}\n      created: Créé\n      action: Action\n      review: Vérification\n    user_role_modal:\n      title: Changer le rôle d'un utilisateur en...\n      btn_cancel: Annuler\n      btn_submit: Valider\n    new_password_modal:\n      title: Définir un nouveau mot de passe\n      form:\n        fields:\n          password:\n            label: Mot de passe\n            text: L'utilisateur sera déconnecté et devra se connecter à nouveau.\n            msg: Le mot de passe doit contenir entre 8 et 32 caractères.\n      btn_cancel: Annuler\n      btn_submit: Envoyer\n    edit_profile_modal:\n      title: Éditer le profil\n      form:\n        fields:\n          display_name:\n            label: Nom affiché\n            msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères.\n          username:\n            label: Nom d'utilisateur\n            msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères.\n          email:\n            label: Email\n            msg_invalid: Adresse e-mail non valide.\n      edit_success: Modifié avec succès\n      btn_cancel: Annuler\n      btn_submit: Soumettre\n    user_modal:\n      title: Ajouter un nouvel utilisateur\n      form:\n        fields:\n          users:\n            label: Ajouter des utilisateurs en masse\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Séparez « nom, email, mot de passe » par des virgules. Un utilisateur par ligne.\n            msg: \"Veuillez entrer l'email de l'utilisateur, un par ligne.\"\n          display_name:\n            label: Nom affiché\n            msg: Le nom affiché doit avoir une longueur de 2 à 30 caractères.\n          email:\n            label: Email\n            msg: L'email n'est pas valide.\n          password:\n            label: Mot de passe\n            msg: Le mot de passe doit comporter entre 8 et 32 caractères.\n      btn_cancel: Annuler\n      btn_submit: Valider\n    users:\n      title: Utilisateurs\n      name: Nom\n      email: E-mail\n      reputation: Réputation\n      created_at: Date de création\n      delete_at: Date de suppression\n      suspend_at: Date de suspension\n      suspend_until: Suspend until\n      status: Statut\n      role: Rôle\n      action: Action\n      change: Modifier\n      all: Tous\n      staff: Staff\n      more: Plus\n      inactive: Inactif\n      suspended: Suspendu\n      deleted: Supprimé\n      normal: Normal\n      Moderator: Modérateur\n      Admin: Administrateur\n      User: Utilisateur\n      filter:\n        placeholder: \"Filtrer par nom, utilisateur:id\"\n      set_new_password: Définir un nouveau mot de passe\n      edit_profile: Éditer le profil\n      change_status: Modifier le statut\n      change_role: Modifier le rôle\n      show_logs: Voir les logs\n      add_user: Ajouter un utilisateur\n      deactivate_user:\n        title: Désactiver l'utilisateur\n        content: Un utilisateur inactif doit revalider son email.\n      delete_user:\n        title: Supprimer cet utilisateur\n        content: Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est définitive !\n        remove: Supprimer leur contenu\n        label: Supprimer toutes les questions, réponses, commentaires, etc.\n        text: Ne cochez pas cette case si vous souhaitez seulement supprimer le compte de l'utilisateur.\n      suspend_user:\n        title: Suspendre cet utilisateur\n        content: Un utilisateur suspendu ne peut pas se connecter.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Non listé\n      post: Publication\n      votes: Votes\n      answers: Réponses\n      created: Créé\n      status: Statut\n      action: Action\n      change: Modifier\n      pending: En attente de traitement\n      filter:\n        placeholder: \"Filtrer par titre, question:id\"\n    answers:\n      page_title: Réponses\n      post: Publication\n      votes: Votes\n      created: Créé\n      status: Statut\n      action: Action\n      change: Modifier\n      filter:\n        placeholder: \"Filtrer par titre, question:id\"\n    general:\n      page_title: Général\n      name:\n        label: Nom du site\n        msg: Le nom ne peut pas être vide.\n        text: \"Le nom de ce site, tel qu'il est utilisé dans la balise titre.\"\n      site_url:\n        label: URL du site\n        msg: L'URL ne peut pas être vide.\n        validate: Indiquez une URL valide.\n        text: L'adresse de ce site.\n      short_desc:\n        label: Courte description du site\n        msg: La description courte ne peut pas être vide.\n        text: \"La description courte, telle qu'elle est utilisée dans le tag titre de la page d'accueil.\"\n      desc:\n        label: Description du site\n        msg: La description du site ne peut pas être vide.\n        text: \"Décrivez ce site en une phrase, telle qu'elle est utilisée dans la balise meta description.\"\n      contact_email:\n        label: Email du contact\n        msg: L'email de contact ne peut pas être vide.\n        validate: L'email de contact n'est pas valide.\n        text: L'adresse email du responsable du site.\n      check_update:\n        label: Mises à jour logicielles\n        text: Rechercher automatiquement les mises à jour\n    interface:\n      page_title: Interface\n      language:\n        label: Langue de l'interface\n        msg: La langue de l'interface ne peut pas être vide.\n        text: Langue de l'interface de l'utilisateur. Cela changera lorsque vous rafraîchissez la page.\n      time_zone:\n        label: Fuseau Horaire\n        msg: Le fuseau horaire ne peut pas être vide.\n        text: Choisissez une ville dans le même fuseau horaire que vous.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: E-mail de l'expéditeur\n        msg: L'email expéditeur ne peut pas être vide.\n        text: L'adresse email à partir de laquelle les emails sont envoyés.\n      from_name:\n        label: Nom de l'expéditeur\n        msg: Le nom expéditeur ne peut pas être vide.\n        text: Le nom d'expéditeur à partir duquel les emails sont envoyés.\n      smtp_host:\n        label: Serveur SMTP\n        msg: Le'hôte SMTP ne peut pas être vide.\n        text: Votre serveur de mail.\n      encryption:\n        label: Chiffrement\n        msg: Le chiffrement ne peut pas être vide.\n        text: Pour la plupart des serveurs, l'option SSL est recommandée.\n        ssl: SSL\n        tls: TLS\n        none: Aucun\n      smtp_port:\n        label: Port SMTP\n        msg: Le port SMTP doit être compris entre 1 et 65535.\n        text: Le port vers votre serveur d'email.\n      smtp_username:\n        label: Utilisateur SMTP\n        msg: Le nom d'utilisateur SMTP ne peut pas être vide.\n      smtp_password:\n        label: Mot de passe SMTP\n        msg: Le mot de passe SMTP ne peut être vide.\n      test_email_recipient:\n        label: Destinataires des e-mails de test\n        text: Indiquez l'adresse email qui recevra l'email de test.\n        msg: Le destinataire de l'email de test est invalide\n      smtp_authentication:\n        label: Activer l'authentification\n        title: Authentification SMTP\n        msg: L'authentification SMTP ne peut pas être vide.\n        \"yes\": \"Oui\"\n        \"no\": \"Non\"\n    branding:\n      page_title: Marque\n      logo:\n        label: Logo\n        msg: Le logo ne peut pas être vide.\n        text: L'image du logo en haut à gauche de votre site. Utilisez une grande image rectangulaire avec une hauteur de 56 et un ratio d'aspect supérieur à 3:1. Si laissé vide, le titre du site sera affiché.\n      mobile_logo:\n        label: Logo pour la version mobile\n        text: Le logo utilisé sur la version mobile de votre site. Utilisez une image rectangulaire large avec une hauteur de 56. Si laissé vide, l'image du paramètre « logo » sera utilisée.\n      square_icon:\n        label: Icône carrée\n        msg: L'icône carrée ne peut pas être vide.\n        text: Image utilisée comme base pour les icônes de métadonnées. Idéalement supérieure à 512x512.\n      favicon:\n        label: Favicon\n        text: Une favicon pour votre site. Pour fonctionner correctement sur un CDN, il doit s'agir d'un png. Sera redimensionné en 32x32. Si laissé vide, « icône carrée » sera utilisé.\n    legal:\n      page_title: Légal\n      terms_of_service:\n        label: Conditions d’utilisation\n        text: \"Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici.\"\n      privacy_policy:\n        label: Protection des données\n        text: \"Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici.\"\n      external_content_display:\n        label: Contenu externe\n        text: \"Le contenu comprend des images, des vidéos et des médias intégrés à partir de sites web externes.\"\n        always_display: Toujours afficher le contenu externe\n        ask_before_display: Demander avant d'afficher le contenu externe\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Écriture de la réponse\n        label: Chaque utilisateur ne peut écrire qu'une seule réponse pour chaque question\n        text: \"Désactivez pour permettre aux utilisateurs d'écrire plusieurs réponses à la même question, ce qui peut causer une perte de concentration des réponses.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Tags recommandés\n        text: \"Les balises recommandées apparaîtront par défaut dans la liste déroulante.\"\n        msg:\n          contain_reserved: \"les tags recommandés ne peuvent pas contenir de tags réservés\"\n      required_tag:\n        title: Définir les tags nécessaires\n        label: Définir les balises « Recommander» comme balises requises\n        text: \"Chaque nouvelle question doit avoir au moins un tag recommandé.\"\n      reserved_tags:\n        label: Tags réservés\n        text: \"Les tags réservés ne peuvent être ajoutés à un message que par un modérateur.\"\n      image_size:\n        label: Taille maximale de l'image (MB)\n        text: \"La taille maximale de téléchargement d'image.\"\n      attachment_size:\n        label: Taille maximale des pièces jointes (MB)\n        text: \"La taille maximale de téléchargement des fichiers joints.\"\n      image_megapixels:\n        label: Max mégapixels image\n        text: \"Nombre maximum de mégapixels autorisés pour une image.\"\n      image_extensions:\n        label: Extensions de pièces jointes autorisées\n        text: \"Une liste d'extensions de fichier autorisées pour l'affichage d'image, séparées par des virgules.\"\n      attachment_extensions:\n        label: Extensions de pièces jointes autorisées\n        text: \"Une liste d'extensions de fichier autorisées pour le téléchargement, séparées par des virgules. ATTENTION : Autoriser les envois peut causer des problèmes de sécurité.\"\n    seo:\n      page_title: Référencement\n      permalink:\n        label: Lien permanent\n        text: Des structures d'URL personnalisées peuvent améliorer la facilité d'utilisation et la compatibilité de vos liens.\n      robots:\n        label: robots.txt\n        text: Ceci remplacera définitivement tous les paramètres liés au site.\n    themes:\n      page_title: Thèmes\n      themes:\n        label: Thèmes\n        text: Sélectionne un thème existant.\n      color_scheme:\n        label: Jeu de couleurs\n      navbar_style:\n        label: Style d'arrière-plan de la barre de navigation\n      primary_color:\n        label: Couleur primaire\n        text: Modifier les couleurs utilisées par vos thèmes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS et HTML\n      custom_css:\n        label: CSS personnalisé\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: En-tête\n        text: >\n\n      footer:\n        label: Pied de page\n        text: Ceci va être inséré avant &lt;/html&gt;.\n      sidebar:\n        label: Panneau latéral\n        text: Cela va être inséré dans la barre latérale.\n    login:\n      page_title: Se connecter\n      membership:\n        title: Adhésion\n        label: Autoriser les inscriptions\n        text: Désactivez pour empêcher quiconque de créer un nouveau compte.\n      email_registration:\n        title: Inscription par e-mail\n        label: Autoriser l'inscription par e-mail\n        text: Désactiver pour empêcher toute personne de créer un nouveau compte par e-mail.\n      allowed_email_domains:\n        title: Domaines d'email autorisés\n        text: Domaines de messagerie avec lesquels les utilisateurs peuvent créer des comptes. Un domaine par ligne. Ignoré si vide.\n      private:\n        title: Privé\n        label: Connexion requise\n        text: Seuls les utilisateurs connectés peuvent accéder à cette communauté.\n      password_login:\n        title: Connexion par mot de passe\n        label: Autoriser la connexion par e-mail et mot de passe\n        text: \"AVERTISSEMENT : Si cette option est désactivée, vous ne pourrez peut-être pas vous connecter si vous n'avez pas configuré une autre méthode de connexion.\"\n    installed_plugins:\n      title: Extensions installées\n      plugin_link: Les plugins étendent les fonctionnalités d'Answer. Vous pouvez trouver des plugins dans le dépôt <1>Answer Plugin Repositor</1>.\n      filter:\n        all: Tous\n        active: Actif\n        inactive: Inactif\n        outdated: Est obsolète\n      plugins:\n        label: Extensions\n        text: Sélectionnez une extension existante.\n      name: Nom\n      version: Versión\n      status: Statut\n      action: Action\n      deactivate: Désactiver\n      activate: Activer\n      settings: Paramètres\n    settings_users:\n      title: Utilisateurs\n      avatar:\n        label: Photo de profil par défaut\n        text: Pour les utilisateurs sans avatar personnalisé.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL de la base de l'API du fournisseur Gravatar. Ignorée lorsqu'elle est vide.\n      profile_editable:\n        title: Profil modifiable\n      allow_update_display_name:\n        label: Permettre aux utilisateurs de changer leur nom d'affichage\n      allow_update_username:\n        label: Permettre aux clients de changer leurs noms d'utilisateur\n      allow_update_avatar:\n        label: Permettre aux utilisateurs de changer leur image de profil\n      allow_update_bio:\n        label: Permettre aux utilisateurs de changer leur biographie\n      allow_update_website:\n        label: Permettre aux utilisateurs de modifier leur site web\n      allow_update_location:\n        label: Permettre aux utilisateurs de modifier leur position\n    privilege:\n      title: Privilèges\n      level:\n        label: Niveau de réputation requis\n        text: Choisissez la réputation requise pour les privilèges\n      msg:\n        should_be_number: l'entrée doit être un nombre\n        number_larger_1: le nombre doit être égal ou supérieur à 1\n    badges:\n      action: Action\n      active: Actif\n      activate: Activer\n      all: Tous\n      awards: Récompenses\n      deactivate: Désactiver\n      filter:\n        placeholder: Filtrer par nom, badge:id\n      group: Groupe\n      inactive: Inactif\n      name: Nom\n      show_logs: Voir les logs\n      status: Statut\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optionnel)\n    empty: ne peut pas être vide\n    invalid: est invalide\n    btn_submit: Sauvegarder\n    not_found_props: \"La propriété requise {{ key }} est introuvable.\"\n    select: Sélectionner\n  page_review:\n    review: Vérifier\n    proposed: proposé\n    question_edit: Modifier la question\n    answer_edit: Modifier la réponse\n    tag_edit: Modifier le tag\n    edit_summary: Modifier le résumé\n    edit_question: Modifier la question\n    edit_answer: Modifier la réponse\n    edit_tag: Modifier l’étiquette\n    empty: Aucune révision restante.\n    approve_revision_tip: Acceptez-vous cette révision?\n    approve_flag_tip: Acceptez-vous ce rapport ?\n    approve_post_tip: Acceptez-vous ce post?\n    approve_user_tip: Acceptez-vous cet utilisateur ?\n    suggest_edits: Modifications suggérées\n    flag_post: Signaler ce message\n    flag_user: Signaler un utilisateur\n    queued_post: Message en file d'attente\n    queued_user: Utilisateur en file d'attente\n    filter_label: Type\n    reputation: réputation\n    flag_post_type: A signalé ce message comme {{ type }}.\n    flag_user_type: A signalé cet utilisateur comme {{ type }}.\n    edit_post: Éditer le post\n    list_post: Lister le post\n    unlist_post: Masquer la post de la liste\n  timeline:\n    undeleted: restauré\n    deleted: supprimé\n    downvote: vote négatif\n    upvote: voter pour\n    accept: accepté\n    cancelled: annulé\n    commented: commenté\n    rollback: Retour arrière (Rollback)\n    edited: modifié\n    answered: répondu\n    asked: demandé\n    closed: fermé\n    reopened: réouvert\n    created: créé\n    pin: épinglé\n    unpin: non épinglé\n    show: listé\n    hide: non listé\n    title: \"Historique de\"\n    tag_title: \"Chronologie de\"\n    show_votes: \"Afficher les votes\"\n    n_or_a: N/A\n    title_for_question: \"Chronologie de\"\n    title_for_answer: \"Chronologie de la réponse à {{ title }} par {{ author }}\"\n    title_for_tag: \"Chronologie pour le tag\"\n    datetime: Date et heure\n    type: Type\n    by: Par\n    comment: Commentaire\n    no_data: \"Nous n'avons rien pu trouver.\"\n  users:\n    title: Utilisateurs\n    users_with_the_most_reputation: Utilisateurs ayant le score de réputation le plus élevé cette semaine\n    users_with_the_most_vote: Utilisateurs qui ont le plus voté cette semaine\n    staffs: Staff de la communauté\n    reputation: réputation\n    votes: votes\n  prompt:\n    leave_page: Voulez-vous vraiment quitter la page ?\n    changes_not_save: Impossible d'enregistrer vos modifications.\n  draft:\n    discard_confirm: Êtes-vous sûr de vouloir abandonner ce brouillon ?\n  messages:\n    post_deleted: Ce message a été supprimé.\n    post_cancel_deleted: Ce post a été restauré.\n    post_pin: Ce message a été épinglé.\n    post_unpin: Ce message a été déépinglé.\n    post_hide_list: Ce message a été masqué de la liste.\n    post_show_list: Ce message a été affiché dans la liste.\n    post_reopen: Ce message a été rouvert.\n    post_list: Ce post a été ajouté à la liste.\n    post_unlist: Ce post a été retiré de la liste.\n    post_pending: Votre message est en attente de révision. C'est un aperçu, il sera visible une fois qu'il aura été approuvé.\n    post_closed: Ce post a été fermé.\n    answer_deleted: Cette réponse a été supprimée.\n    answer_cancel_deleted: Cette réponse a été restaurée.\n    change_user_role: Le rôle de cet utilisateur a été modifié.\n    user_inactive: Cet utilisateur est déjà inactif.\n    user_normal: Cet utilisateur est déjà normal.\n    user_suspended: Cet utilisateur a été suspendu.\n    user_deleted: Cet utilisateur a été supprimé.\n    user_added: User has been added successfully.\n    badge_activated: Ce badge a été activé.\n    badge_inactivated: Ce badge a été désactivé.\n    users_deleted: Ces utilisateurs ont été supprimés.\n    posts_deleted: Ces questions ont été supprimées.\n    answers_deleted: Ces réponses ont été supprimées.\n    copy: Copier dans le presse-papier\n    copied: Copié\n    external_content_warning: Les images/médias externes ne sont pas affichés.\n\n\n"
  },
  {
    "path": "i18n/he_IL.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/hi_IN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Edit\n    delete:\n      other: Delete\n    close:\n      other: Close\n    reopen:\n      other: Reopen\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: List\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Password\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: You cannot modify your password.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Edit Tag\n    ask_a_question: Create Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    new_alerts: New alerts\n    all_read: Mark all as read\n    show_more: Show more\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Select people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/hu_HU.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/hy_AM.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/i18n.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage i18n\n\nimport \"embed\"\n\n//go:embed  *.yaml\nvar I18n embed.FS\n"
  },
  {
    "path": "i18n/i18n.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# all support language\nlanguage_options:\n  - label: \"English\"\n    value: \"en_US\"\n    progress: 100\n  - label: \"Español\"\n    value: \"es_ES\"\n    progress: 96\n  - label: \"Português(BR)\"\n    value: \"pt_BR\"\n    progress: 96\n  - label: \"Português\"\n    value: \"pt_PT\"\n    progress: 96\n  - label: \"Deutsch\"\n    value: \"de_DE\"\n    progress: 96\n  - label: \"Français\"\n    value: \"fr_FR\"\n    progress: 96\n  - label: \"日本語\"\n    value: \"ja_JP\"\n    progress: 96\n  - label: \"Italiano\"\n    value: \"it_IT\"\n    progress: 96\n  - label: \"Русский\"\n    value: \"ru_RU\"\n    progress: 80\n  - label: \"简体中文\"\n    value: \"zh_CN\"\n    progress: 100\n  - label: \"繁體中文\"\n    value: \"zh_TW\"\n    progress: 47\n  - label: \"한국어\"\n    value: \"ko_KR\"\n    progress: 73\n  - label: \"Tiếng Việt\"\n    value: \"vi_VN\"\n    progress: 96\n  - label: \"Slovak\"\n    value: \"sk_SK\"\n    progress: 45\n  - label: \"فارسی\"\n    value: \"fa_IR\"\n    progress: 69\n"
  },
  {
    "path": "i18n/id_ID.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Sukses.\n    unknown:\n      other: Kesalahan tidak diketahui.\n    request_format_error:\n      other: Permintaan tidak sah.\n    unauthorized_error:\n      other: Tidak diizinkan.\n    database_error:\n      other: Kesalahan data server.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Ubah\n    delete:\n      other: Hapus\n    close:\n      other: Tutup\n    reopen:\n      other: Buka kembali\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: Daftar\n    invite_someone_to_answer:\n      other: Undang seseorang untuk menjawab\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Pengguna\n      admin:\n        other: Administrator\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default tanpa akses khusus.\n      admin:\n        other: Memiliki kontrol penuh atas situs.\n      moderator:\n        other: Memiliki kendali atas semua kiriman kecuali pengaturan administrator.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Tanyakan sesuatu\n    rank_answer_add_label:\n      other: Tulis jawaban\n    rank_comment_add_label:\n      other: Tulis komentar\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Kirim lebih dari 2 tautan secara bersamaan\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Undang seseorang untuk menjawab\n    rank_tag_add_label:\n      other: Buat tag baru\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Kata sandi\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: '\"Email\" dan kata sandi tidak cocok.'\n  error:\n    common:\n      invalid_url:\n        other: URL salah.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password tidak boleh mengandung spasi.\n    admin:\n      cannot_update_their_password:\n        other: You cannot modify your password.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: Email dan kata sandi tidak cocok.\n    answer:\n      not_found:\n        other: Jawaban tidak ditemukan.\n      cannot_deleted:\n        other: Tidak memiliki izin untuk menghapus.\n      cannot_update:\n        other: Tidak memiliki izin untuk memperbarui.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Tidak diizinkan untuk mengubah komentar.\n      not_found:\n        other: Komentar tidak ditemukan.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email telah terdaftar.\n      need_to_be_verified:\n        other: Email harus terverifikasi.\n      verify_url_expired:\n        other: URL verifikasi email telah kadaluwarsa, silahkan kirim ulang.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Bahasa tidak ditemukan.\n    object:\n      captcha_verification_failed:\n        other: Captcha salah.\n      disallow_follow:\n        other: Anda tidak diizinkan untuk mengikuti.\n      disallow_vote:\n        other: Anda tisak diizinkan untuk melakukan vote.\n      disallow_vote_your_self:\n        other: Anda tidak dapat melakukan voting untuk ulasan Anda sendiri.\n      not_found:\n        other: Objek tidak ditemukan.\n      verification_failed:\n        other: Verifikasi gagal.\n      email_or_password_incorrect:\n        other: Email dan kata sandi tidak cocok.\n      old_password_verification_failed:\n        other: Verifikasi password lama, gagal\n      new_password_same_as_previous_setting:\n        other: Kata sandi baru sama dengan kata sandi yang sebelumnya.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Pertanyaan tidak ditemukan.\n      cannot_deleted:\n        other: Tidak memiliki izin untuk menghapus.\n      cannot_close:\n        other: Tidak diizinkan untuk menutup.\n      cannot_update:\n        other: Tidak diizinkan untuk memperbarui.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Laporan penanganan gagal.\n      not_found:\n        other: Laporan tidak ditemukan.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag tidak ditemukan.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Silahkan isi setidaknya satu tag yang diperlukan.\n      not_contain_synonym_tags:\n        other: Tidak boleh mengandung Tag sinonim.\n      cannot_update:\n        other: Tidak memiliki izin untuk memperbaharui.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: Anda tidak bisa menetapkan sinonim dari tag saat ini dengan tag yang sama.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Tema tidak ditemukan.\n    revision:\n      review_underway:\n        other: Tidak dapat mengedit saat ini, sedang ada review versi pada antrian.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email dan kata sandi tidak cocok.\n      not_found:\n        other: Pengguna tidak ditemukan.\n      suspended:\n        other: Pengguna ini telah ditangguhkan.\n      username_invalid:\n        other: Nama pengguna tidak sesuai.\n      username_duplicate:\n        other: Nama pengguna sudah digunakan.\n      set_avatar:\n        other: Set avatar gagal.\n      cannot_update_your_role:\n        other: Anda tidak dapat mengubah peran anda sendiri.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Gagal membaca konfigurasi\n    database:\n      connection_failed:\n        other: Koneksi ke database gagal\n      create_table_failed:\n        other: Gagal membuat tabel\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Pertanyaan ini telah ditanyakan sebelumnya dan sudah ada jawabannya.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: Pertanyaan ini tidak sesuai dengan pedoman komunitas.\n      multiple:\n        name:\n          other: membutuhkan detail atau kejelasan\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: lainnya\n        desc:\n          other: Posting ini membutuhkan alasan lain yang tidak tercantum di atas.\n    operation_type:\n      asked:\n        other: ditanyakan\n      answered:\n        other: dijawab\n      modified:\n        other: dimodifikasi\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: pertanyaan yang diperbaharui\n      answer_the_question:\n        other: pertanyaan yang dijawab\n      update_answer:\n        other: jawaban yang diperbaharui\n      accept_answer:\n        other: pertanyaan yanag diterima\n      comment_question:\n        other: pertanyaan yang dikomentari\n      comment_answer:\n        other: jawaban yang dikomentari\n      reply_to_you:\n        other: membalas Anda\n      mention_you:\n        other: menyebutmu\n      your_question_is_closed:\n        other: Pertanyaanmu telah ditutup\n      your_question_was_deleted:\n        other: Pertanyaanmu telah dihapus\n      your_answer_was_deleted:\n        other: Jawabanmu telah dihapus\n      your_comment_was_deleted:\n        other: Komentarmu telah dihapus\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Cara memformat\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Sebelumnya\n    next: Selanjutnya\n  page_title:\n    question: Pertanyaan\n    questions: Pertanyaan\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Ubah Tag\n    ask_a_question: Create Question\n    edit_question: Sunting Pertanyaan\n    edit_answer: Sunting jawaban\n    search: Cari\n    posts_containing: Postingan mengandung\n    settings: Pengaturan\n    notifications: Pemberitahuan\n    login: Log In\n    sign_up: Daftar\n    account_recovery: Pemulihan Akun\n    account_activation: Aktivasi Akun\n    confirm_email: Konfirmasi email\n    account_suspended: Akun Ditangguhkan\n    admin: Admin\n    change_email: Modifikasi email\n    install: Instalasi Answer\n    upgrade: Meng-upgrade Answer\n    maintenance: Pemeliharaan Website\n    users: Pengguna\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Pemberitahuan\n    inbox: Kotak Masuk\n    achievement: Pencapaian\n    new_alerts: New alerts\n    all_read: Tandai Semua Jika Sudah Dibaca\n    show_more: Tampilkan lebih banyak\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Akun Anda telah ditangguhkan\n    until_time: \"Akun anda ditangguhkan sampai {{ time }}.\"\n    forever: Pengguna ini ditangguhkan selamanya.\n    end: Anda tidak sesuai dengan syarat pedoman komunitas.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Diagram alir\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Tambahkan sample code\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code tidak boleh kosong.\n          language:\n            label: Language\n            placeholder: Deteksi otomatis\n      btn_cancel: Batal\n      btn_confirm: Tambah\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Gambar\n      add_image: Tambahkan gambar\n      tab_image: Unggah gambar\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Pilih gambar\n            msg:\n              empty: File tidak boleh kosong.\n              only_image: Hanya file Gambar yang diperbolehkan.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: URL gambar\n      form_url:\n        fields:\n          url:\n            label: URL gambar\n            msg:\n              empty: URL gambar tidak boleh kosong.\n          name:\n            label: Description\n      btn_cancel: Batal\n      btn_confirm: Tambah\n      uploading: Sedang mengunggah\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Batal\n      btn_confirm: Tambah\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Postingan ini saya tutup sebagai...\n    btn_cancel: Batal\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Dibuat\n    edited_at: Disunting\n    history: Riwayat\n    synonyms:\n      title: Sinonim\n      text: Tag berikut akan dipetakan ulang ke\n      empty: Sinonim tidak ditemukan.\n      btn_add: Tambahkan sinonim\n      btn_edit: Sunting\n      btn_save: Simpan\n    synonyms_text: Tag berikut akan dipetakan ulang ke\n    delete:\n      title: Hapus tagar ini\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Tutup\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Ubah Tag\n    default_reason: Sunting tag\n    default_first_reason: Add tag\n    btn_save_edits: Simpan suntingan\n    btn_cancel: Batal\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: sekarang\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: jam\n    day: hari\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Tambahkan Komentar\n    reply_to: Balas ke\n    btn_reply: Balas\n    btn_edit: Sunting\n    btn_delete: Hapus\n    btn_flag: Flag\n    btn_save_edits: Simpan suntingan\n    btn_cancel: Batal\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Gunakan komentar untuk meminta informasi lebih lanjut atau menyarankan perbaikan. Hindari menjawab pertanyaan di komentar.\n    tip_answer: >-\n      Gunakan komentar untuk membalas pengguna lain atau memberi tahu mereka tentang perubahan. Jika Anda menambahkan informasi baru, cukup edit posting Anda.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Sunting jawaban\n    default_reason: Edit jawaban\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revisi\n        answer:\n          label: Jawaban\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Pengguna\n      badges: Badges\n      profile: Profil\n      setting: Pengaturan\n      logout: Keluar\n      admin: Admin\n      review: Ulasan\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Cari\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Ubah\n    loading: sedang memuat...\n  pic_auth_code:\n    title: Capthcha\n    placeholder: Masukkan teks di atas\n    msg:\n      empty: Captcha tidak boleh kosong.\n  inactive:\n    first: >-\n      Kamu hampir selesai! Kami telah mengirimkan email aktivasi ke <bold>{{mail}}</bold>. Silakan ikuti petunjuk dalam email untuk mengaktifkan akun Anda.\n    info: \"Jika tidak ada email masuk, mohon periksa folder spam Anda.\"\n    another: >-\n      Kami telah mengirimkan email aktivasi lain kepada Anda di <bold>{{mail}}</bold>. Mungkin butuh beberapa menit untuk tiba; pastikan untuk memeriksa folder spam Anda.\n    btn_name: Kirim ulang email aktivasi\n    change_btn_name: Ganti email\n    msg:\n      empty: Tidak bisa kosong.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Masuk untuk melanjutkan\n    info_sign: Belum punya akun? <1>Daftar</1>\n    info_login: Sudah punya akun? <1>Masuk</1>\n    agreements: Dengan mendaftar, Anda menyetujui <1>kebijakan privasi</1> dan <3>persyaratan layanan</3>.\n    forgot_pass: Lupa password?\n    name:\n      label: Nama\n      msg:\n        empty: Nama tidak boleh kosong.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email tidak boleh kosong.\n    password:\n      label: Kata sandi\n      msg:\n        empty: Kata sandi tidak boleh kosong.\n        different: Kata sandi yang dimasukkan tidak sama\n  account_forgot:\n    page_title: Lupa kata sandi Anda\n    btn_name: Tulis email pemulihan\n    send_success: >-\n      Jika akun cocok dengan <strong>{{mail}}</strong>, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi.\n    email:\n      label: Email\n      msg:\n        empty: Email tidak boleh kosong.\n  change_email:\n    btn_cancel: Batal\n    btn_update: Perbarui alamat email\n    send_success: >-\n      Jika akun cocok dengan <strong>{{mail}}</strong>, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi.\n    email:\n      label: New email\n      msg:\n        empty: Email tidak boleh kosong.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Atur ulang kata sandi\n    btn_name: Atur ulang kata sandi saya\n    reset_success: >-\n      Anda berhasil mengubah kata sandi Anda; Anda akan dialihkan ke halaman login.\n    link_invalid: >-\n      Maaf, link setel ulang sandi ini sudah tidak valid. Mungkin kata sandi Anda sudah diatur ulang?\n    to_login: Lanjutkan ke halaman Login\n    password:\n      label: Kata sandi\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: Bahasa antarmuka pengguna. Itu akan berubah ketika Anda me-refresh halaman.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: pembaruan sukses\n    update_password: Kata sandi berhasil diganti.\n    flag_success: Terima kasih telah menandai.\n    forbidden_operate_self: Dilarang melakukan operasi ini pada diri sendiri\n    review: Revisi Anda akan ditampilkan setelah ditinjau.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: jawaban\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Select people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Ditanyakan\n    asked: ditanyakan\n    update: Diubah\n    Edited: Edited\n    edit: disunting\n    commented: commented\n    Views: Dilihat\n    Follow: Ikuti\n    Following: Mengikuti\n    follow_tip: Follow this question to receive notifications\n    answered: dijawab\n    closed_in: Ditutup pada\n    show_exist: Gunakan pertanyaan yang sudah ada.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Jawaban\n      score: Nilai\n      newest: Terbaru\n      oldest: Oldest\n      btn_accept: Terima\n      btn_accepted: Diterima\n    write_answer:\n      title: Jawaban Anda\n      edit_answer: Edit my existing answer\n      btn_name: Kirimkan jawaban Anda\n      add_another_answer: Tambahkan jawaban lain\n      confirm_title: Lanjutkan menjawab\n      continue: Lanjutkan\n      confirm_info: >-\n        <p>Yakin ingin menambahkan jawaban lain?</p><p>Sebagai gantinya, Anda dapat menggunakan tautan edit untuk menyaring dan menyempurnakan jawaban anda.</p>\n      empty: Jawaban tidak boleh kosong.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Buka kembali postingan ini\n      content: Kamu yakin ingin membuka kembali?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Hapus pos ini\n    question: >-\n      Kami tidak menyarankan <strong>menghapus pertanyaan dengan jawaban</strong> karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.</p><p>Penghapusan berulang atas pertanyaan yang dijawab dapat mengakibatkan akun Anda diblokir untuk bertanya. Apakah Anda yakin ingin menghapus?\n    answer_accepted: >-\n      <p>Kami tidak menyarankan <strong>menghapus jawaban yang diterima</strong> karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang. </p> Penghapusan berulang dari jawaban yang diterima dapat menyebabkan akun Anda diblokir dari menjawab. Apakah Anda yakin ingin menghapus?\n    other: Anda yakin ingin menghapusnya?\n    tip_answer_deleted: Jawaban ini telah dihapus\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Konfirmasi\n    cancel: Batal\n    edit: Edit\n    save: Simpan\n    delete: Hapus\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Masuk\n    signup: Daftar\n    logout: Keluar\n    verify: Verifikasi\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Jawaban\n    newest: Terbaru\n    active: Aktif\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Nilai\n    unanswered: Belum dijawab\n    modified: diubah\n    answered: dijawab\n    asked: ditanyakan\n    closed: ditutup\n    follow_a_tag: Ikuti tagar\n    more: Lebih\n  personal:\n    overview: Ringkasan\n    answers: Jawaban\n    answer: jawaban\n    questions: Pertanyaan\n    question: pertanyaan\n    bookmarks: Bookmarks\n    reputation: Reputasi\n    comments: Komentar\n    votes: Vote\n    badges: Badges\n    newest: Terbaru\n    score: Nilai\n    edit_profile: Edit profile\n    visited_x_days: \"Dikunjungi {{ count }} hari\"\n    viewed: Dilihat\n    joined: Bergabung\n    comma: \",\"\n    last_login: Dilihat\n    about_me: Tentang Saya\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Jawaban terpopuler\n    top_questions: Pertanyaan terpopuler\n    stats: Statistik\n    list_empty: Postingan tidak ditemukan.<br />Mungkin Anda ingin memilih tab lain?\n    content_empty: No posts found.\n    accepted: Diterima\n    answered: dijawab\n    asked: ditanyakan\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderator\n    x_reputation: reputasi\n    x_votes: vote diterima\n    x_answers: jawaban\n    x_questions: pertanyaan\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Selanjutnya\n    done: Selesai\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Koneksi ke database gagal\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dasbor\n    contents: Konten\n    questions: Pertanyaan\n    answers: Jawaban\n    users: Pengguna\n    badges: Badges\n    flags: Flags\n    settings: Pengaturan\n    general: Umum\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privasi\n    seo: SEO\n    customize: Kostumisasi\n    themes: Tema\n    login: Masuk\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dasbor\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Pertanyaan:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Jawaban:\"\n      comments: \"Komentar:\"\n      votes: \"Vote:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Versi:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Zona Waktu:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Penyimpanan yang terpakai:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Dokumen\n      feedback: Masukan\n      support: Dukungan\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Ubah\n      all: Semua\n      staff: Staf\n      more: More\n      inactive: Tidak Aktif\n      suspended: Ditangguhkan\n      deleted: Dihapus\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: Pengguna\n      filter:\n        placeholder: \"Filter berdasarkan nama, user:id\"\n      set_new_password: Atur password baru\n      edit_profile: Edit profile\n      change_status: Ubah status\n      change_role: Ubah role\n      show_logs: Tampilkan log\n      add_user: Tambahkan pengguna\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Pertanyaan\n      unlisted: Unlisted\n      post: Post\n      votes: Vote\n      answers: Jawaban\n      created: Dibuat\n      status: Status\n      action: Action\n      change: Ubah\n      pending: Pending\n      filter:\n        placeholder: \"Filter berdasarkan judul, question:id\"\n    answers:\n      page_title: Jawaban\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Enkripsi\n        msg: Enkripsi tidak boleh kosong.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: Tidak ada\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: Port untuk server email Anda.\n      smtp_username:\n        label: SMTP username\n        msg: Nama Pengguna SMTP tidak boleh kosong.\n      smtp_password:\n        label: SMTP password\n        msg: Password SMTP tidak boleh kosong.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/it_IT.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Successo.\n    unknown:\n      other: Errore sconosciuto.\n    request_format_error:\n      other: Il formato della richiesta non è valido.\n    unauthorized_error:\n      other: Non autorizzato.\n    database_error:\n      other: Errore nel server dati.\n    forbidden_error:\n      other: Vietato.\n    duplicate_request_error:\n      other: Duplica invio.\n  action:\n    report:\n      other: Segnala\n    edit:\n      other: Modifica\n    delete:\n      other: Cancella\n    close:\n      other: Chiudi\n    reopen:\n      other: Riapri\n    forbidden_error:\n      other: Vietato.\n    pin:\n      other: Fissa sul profilo\n    hide:\n      other: Rimuovi dall'elenco\n    unpin:\n      other: Stacca dal profilo\n    show:\n      other: Aggiungi all'elenco\n    invite_someone_to_answer:\n      other: Modifica\n    undelete:\n      other: Ripristina\n    merge:\n      other: Unisci\n  role:\n    name:\n      user:\n        other: Utente\n      admin:\n        other: Amministratore\n      moderator:\n        other: Moderatore\n    description:\n      user:\n        other: Predefinito senza alcun accesso speciale.\n      admin:\n        other: Avere il pieno potere di accedere al sito.\n      moderator:\n        other: Ha accesso a tutti i post tranne le impostazioni di amministratore.\n  privilege:\n    level_1:\n      description:\n        other: 'Livello 1 (per team o gruppi privati: richiesta reputazione limitata)'\n    level_2:\n      description:\n        other: 'Livello 2 (per startup community: richiesta reputazione scarsa)'\n    level_3:\n      description:\n        other: 'Livello 3 (per community riconosciute: richiesta reputazione alta)'\n    level_custom:\n      description:\n        other: Livello personalizzato\n    rank_question_add_label:\n      other: Fai una domanda\n    rank_answer_add_label:\n      other: Scrivi una risposta\n    rank_comment_add_label:\n      other: Scrivi un commento\n    rank_report_add_label:\n      other: Segnala\n    rank_comment_vote_up_label:\n      other: Approva commento\n    rank_link_url_limit_label:\n      other: Pubblica più di 2 link alla volta\n    rank_question_vote_up_label:\n      other: Approva domanda\n    rank_answer_vote_up_label:\n      other: Approva risposta\n    rank_question_vote_down_label:\n      other: Disapprova domanda\n    rank_answer_vote_down_label:\n      other: Disapprova risposta\n    rank_invite_someone_to_answer_label:\n      other: Invita qualcuno a rispondere\n    rank_tag_add_label:\n      other: Crea un nuovo tag\n    rank_tag_edit_label:\n      other: Modifica descrizione tag (necessità di revisione)\n    rank_question_edit_label:\n      other: Modifica la domanda di altri (necessità di revisione)\n    rank_answer_edit_label:\n      other: Modifica la risposta di altri (necessità di revisione)\n    rank_question_edit_without_review_label:\n      other: Modifica la domanda di altri senza bisogno di revisione\n    rank_answer_edit_without_review_label:\n      other: Modifica la risposta di altri senza bisogno di revisione\n    rank_question_audit_label:\n      other: Rivedi modifiche alla domanda\n    rank_answer_audit_label:\n      other: Rivedi modifiche alla risposta\n    rank_tag_audit_label:\n      other: Esamina le modifiche ai tag\n    rank_tag_edit_without_review_label:\n      other: Modifica la descrizione del tag senza necessità di revisione\n    rank_tag_synonym_label:\n      other: Gestisci sinonimi dei tag\n  email:\n    other: E-mail\n  e_mail:\n    other: E-mail\n  password:\n    other: Chiave di accesso\n  pass:\n    other: Password\n  old_pass:\n    other: Password attuale\n  original_text:\n    other: Questo post\n  email_or_password_wrong_error:\n    other: Email o password errati.\n  error:\n    common:\n      invalid_url:\n        other: URL non valido.\n      status_invalid:\n        other: Status non valido.\n    password:\n      space_invalid:\n        other: La password non può contenere spazi.\n    admin:\n      cannot_update_their_password:\n        other: Non è possibile modificare la password.\n      cannot_edit_their_profile:\n        other: Non è possibile modificare il profilo.\n      cannot_modify_self_status:\n        other: Non è possibile modificare il tuo status.\n      email_or_password_wrong:\n        other: Email o password errati.\n    answer:\n      not_found:\n        other: Risposta non trovata.\n      cannot_deleted:\n        other: Permesso per cancellare mancante.\n      cannot_update:\n        other: Nessun permesso per l'aggiornamento.\n      question_closed_cannot_add:\n        other: Le domande sono chiuse e non possono essere aggiunte.\n      content_cannot_empty:\n        other: Il contenuto della risposta non può essere vuoto.\n    comment:\n      edit_without_permission:\n        other: Non si hanno di privilegi sufficienti per modificare il commento.\n      not_found:\n        other: Commento non trovato.\n      cannot_edit_after_deadline:\n        other: Il tempo per editare è scaduto.\n      content_cannot_empty:\n        other: Il commento non può essere vuoto.\n    email:\n      duplicate:\n        other: Email già esistente.\n      need_to_be_verified:\n        other: L'email deve essere verificata.\n      verify_url_expired:\n        other: L'url di verifica email è scaduto, si prega di reinviare l'email.\n      illegal_email_domain_error:\n        other: L'email non è consentita da quel dominio di posta elettronica. Si prega di usarne un altro.\n    lang:\n      not_found:\n        other: File lingua non trovato.\n    object:\n      captcha_verification_failed:\n        other: Captcha errato.\n      disallow_follow:\n        other: Non sei autorizzato a seguire\n      disallow_vote:\n        other: non sei autorizzato a votare\n      disallow_vote_your_self:\n        other: Non puoi votare un tuo post!\n      not_found:\n        other: oggetto non trovato\n      verification_failed:\n        other: verifica fallita\n      email_or_password_incorrect:\n        other: email o password incorretti\n      old_password_verification_failed:\n        other: la verifica della vecchia password è fallita\n      new_password_same_as_previous_setting:\n        other: La nuova password è identica alla precedente\n      already_deleted:\n        other: Questo post è stato eliminato.\n    meta:\n      object_not_found:\n        other: Meta oggetto non trovato\n    question:\n      already_deleted:\n        other: Questo post è stato eliminato.\n      under_review:\n        other: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato.\n      not_found:\n        other: domanda non trovata\n      cannot_deleted:\n        other: Permesso per cancellare mancante.\n      cannot_close:\n        other: Nessun permesso per chiudere.\n      cannot_update:\n        other: Nessun permesso per l'aggiornamento.\n      content_cannot_empty:\n        other: Il contenuto non può essere vuoto.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Il rango di reputazione non soddisfa le condizioni.\n      vote_fail_to_meet_the_condition:\n        other: Grazie per il feedback. Hai bisogno di almeno una reputazione {{.Rank}}  per votare.\n      no_enough_rank_to_operate:\n        other: Hai bisogno di almeno una reputazione {{.Rank}} per fare questo.\n    report:\n      handle_failed:\n        other: Gestione del report fallita\n      not_found:\n        other: Report non trovato\n    tag:\n      already_exist:\n        other: Tag già esistente.\n      not_found:\n        other: Etichetta non trovata\n      recommend_tag_not_found:\n        other: Il tag consigliato non esiste.\n      recommend_tag_enter:\n        other: Inserisci almeno un tag.\n      not_contain_synonym_tags:\n        other: Non deve contenere tag sinonimi.\n      cannot_update:\n        other: Nessun permesso per l'aggiornamento.\n      is_used_cannot_delete:\n        other: Non è possibile eliminare un tag in uso.\n      cannot_set_synonym_as_itself:\n        other: Non puoi impostare il sinonimo del tag corrente come se stesso.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Il mittente non può essere un indirizzo email.\n    theme:\n      not_found:\n        other: tema non trovato\n    revision:\n      review_underway:\n        other: Non è possibile modificare al momento, c'è una versione nella coda di revisione.\n      no_permission:\n        other: Non è permessa la revisione.\n    user:\n      external_login_missing_user_id:\n        other: La piattaforma di terze parti non fornisce un utente ID unico, quindi non è possibile effettuare il login. Si prega di contattare l'amministratore del sito web.\n      external_login_unbinding_forbidden:\n        other: Per favore imposta una password di login per il tuo account prima di rimuovere questo accesso.\n      email_or_password_wrong:\n        other:\n          other: Email o password errati\n      not_found:\n        other: utente non trovato\n      suspended:\n        other: utente sospeso\n      username_invalid:\n        other: utente non valido\n      username_duplicate:\n        other: Nome utente già in uso\n      set_avatar:\n        other: Inserimento dell'Avatar non riuscito.\n      cannot_update_your_role:\n        other: Non puoi modificare il tuo ruolo.\n      not_allowed_registration:\n        other: Attualmente il sito non è aperto per la registrazione.\n      not_allowed_login_via_password:\n        other: Attualmente non è consentito accedere al sito tramite password.\n      access_denied:\n        other: Accesso negato\n      page_access_denied:\n        other: Non hai accesso a questa pagina.\n      add_bulk_users_format_error:\n        other: \"Errore {{.Field}} formato vicino a '{{.Content}}' alla riga {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Il numero di utenti che aggiungi contemporaneamente dovrebbe essere compreso tra 1 e {{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>Questo utente è stato sospeso per sempre.</strong> Questo utente non soddisfa le linee guida della comunità.\"\n      status_suspended_until:\n        other: \"<strong>Questo utente è stato sospeso fino a {{.SuspendedUntil}}.</strong> Questo utente non soddisfa le linee guida della comunità.\"\n      status_deleted:\n        other: \"Utente eliminato.\"\n      status_inactive:\n        other: \"L'utente è inattivo.\"\n    config:\n      read_config_failed:\n        other: Configurazione lettura fallita\n    database:\n      connection_failed:\n        other: Connessione al database fallita\n      create_table_failed:\n        other: Creazione tabella non riuscita\n    install:\n      create_config_failed:\n        other: Impossibile creare il file config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Formato file non supportato.\n    site_info:\n      config_not_found:\n        other: Configurazione del sito non trovata.\n    badge:\n      object_not_found:\n        other: Oggetto badge non trovato\n  reason:\n    spam:\n      name:\n        other: Spam\n      desc:\n        other: \"Questo post è una pubblicità o spam. Non è utile o rilevante per l'argomento attuale.\\n\"\n    rude_or_abusive:\n      name:\n        other: scortese o offensivo\n      desc:\n        other: \"Una persona ragionevole troverebbe questo contenuto inappropriato per un discorso rispettoso.\"\n    a_duplicate:\n      name:\n        other: duplicato\n      desc:\n        other: Questa domanda è già stata posta e ha già una risposta.\n      placeholder:\n        other: Inserisci il link alla domanda esistente\n    not_a_answer:\n      name:\n        other: Non è una risposta\n      desc:\n        other: \"Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda,o cancellato del tutto.\"\n    no_longer_needed:\n      name:\n        other: Non più necessario\n      desc:\n        other: Questo commento è obsoleto, informale o non rilevante per questo post.\n    something:\n      name:\n        other: Qualcos'altro\n      desc:\n        other: Questo post richiede l'attenzione dello staff per un altro motivo non elencato sopra.\n      placeholder:\n        other: Facci sapere nello specifico cosa ti preoccupa\n    community_specific:\n      name:\n        other: Un motivo legato alla community\n      desc:\n        other: Questa domanda non soddisfa le linee guida della community.\n    not_clarity:\n      name:\n        other: Richiede maggiori dettagli o chiarezza\n      desc:\n        other: Questa domanda include più domande in una. Dovrebbe concentrarsi su un unico problema.\n    looks_ok:\n      name:\n        other: sembra OK\n      desc:\n        other: Questo post è corretto così com'è e non è di bassa qualità.\n    needs_edit:\n      name:\n        other: Necessita di modifiche che ho  effettuato\n      desc:\n        other: Migliora e correggi tu stesso i problemi di questo post.\n    needs_close:\n      name:\n        other: necessita di chiusura\n      desc:\n        other: A una domanda chiusa non è possibile rispondere, ma è comunque possibile modificare, votare e commentare.\n    needs_delete:\n      name:\n        other: Necessario eliminare\n      desc:\n        other: Questo post verrà eliminato.\n  question:\n    close:\n      duplicate:\n        name:\n          other: posta indesiderata\n        desc:\n          other: Questa domanda è già stata posta e ha già una risposta.\n      guideline:\n        name:\n          other: motivo legato alla community\n        desc:\n          other: Questa domanda non soddisfa le linee guida della comunità.\n      multiple:\n        name:\n          other: richiede maggiori dettagli o chiarezza\n        desc:\n          other: Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema.\n      other:\n        name:\n          other: altro\n        desc:\n          other: Questo articolo richiede un'altro motivo non listato sopra.\n    operation_type:\n      asked:\n        other: chiesto\n      answered:\n        other: Risposto\n      modified:\n        other: Modificato\n    deleted_title:\n      other: \"\\nDomanda cancellata\"\n    questions_title:\n      other: Domande\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: Il tag non ha descrizioni.\n  notification:\n    action:\n      update_question:\n        other: domanda aggiornata\n      answer_the_question:\n        other: domanda risposta\n      update_answer:\n        other: risposta aggiornata\n      accept_answer:\n        other: risposta accettata\n      comment_question:\n        other: domanda commentata\n      comment_answer:\n        other: risposta commentata\n      reply_to_you:\n        other: hai ricevuto risposta\n      mention_you:\n        other: sei stato menzionato\n      your_question_is_closed:\n        other: la tua domanda è stata chiusa\n      your_question_was_deleted:\n        other: la tua domanda è stata rimossa\n      your_answer_was_deleted:\n        other: la tua risposta è stata rimossa\n      your_comment_was_deleted:\n        other: il tuo commento è stato rimosso\n      up_voted_question:\n        other: domanda approvata\n      down_voted_question:\n        other: domanda scartata\n      up_voted_answer:\n        other: risposta approvata\n      down_voted_answer:\n        other: risposta sfavorevole\n      up_voted_comment:\n        other: commento approvato\n      invited_you_to_answer:\n        other: sei invitato a rispondere\n      earned_badge:\n        other: Hai ottenuto il badge \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Conferma il tuo nuovo indirizzo email\"\n      body:\n        other: \"Conferma il tuo nuovo indirizzo email per {{.SiteName}} cliccando sul seguente link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nSe non hai richiesto questa modifica, ignora questa email.<br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} ha risposto alla tua domanda\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Visualizza su {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancellazione</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} ti ha invitato a rispondere\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Penso che tu possa sapere la risposta.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Visualizza su {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancellazione</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} ha commentato il tuo post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Visualizza su {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancellazione</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nuova domanda: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non sarà visualizzata.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancellati</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Reimpostazione della password\"\n      body:\n        other: \"Qualcuno ha chiesto di reimpostare la tua password su {{.SiteName}}.<br><br>\\n\\nSe non sei tu, puoi tranquillamente ignorare questa email.<br><br>\\n\\nClicca sul seguente link per scegliere una nuova password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Conferma il tuo nuovo account\"\n      body:\n        other: \"Benvenuto in {{.SiteName}}!<br><br>\\n\\nClicca il seguente link per confermare e attivare il tuo nuovo account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nSe il link di cui sopra non è cliccabile, prova a copiarlo e incollarlo nella barra degli indirizzi del tuo browser web.\\n<br><br>\\n\\n--<br>\\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Email di prova\"\n      body:\n        other: \"Questa è una email di prova.\\n<br><br>\\n\\n--<br>\\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.\"\n  action_activity_type:\n    upvote:\n      other: voto a favore\n    upvoted:\n      other: voto a favore\n    downvote:\n      other: voto negativo\n    downvoted:\n      other: votato negativamente\n    accept:\n      other: Accetta\n    accepted:\n      other: Accettato\n    edit:\n      other: modifica\n  review:\n    queued_post:\n      other: Post in coda\n    flagged_post:\n      other: Post contrassegnato\n    suggested_post_edit:\n      other: Modifiche suggerite\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} e {{ .Count }} più...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiografo\n        desc:\n          other: Informazioni sul profilo <a href=\"{{ .ProfileURL }}\" target=\"_blank\"></a> completate.\n      certified:\n        name:\n          other: Certificato\n        desc:\n          other: \"\\nCompletato il nostro nuovo tutorial per l'utente.\"\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: Prima modifica al post.\n      first_flag:\n        name:\n          other: Primo Contrassegno\n        desc:\n          other: Primo contrassegno di un post.\n      first_upvote:\n        name:\n          other: Primo Mi Piace\n        desc:\n          other: Primo Mi Piace a un post\n      first_link:\n        name:\n          other: Primo Link\n        desc:\n          other: Per prima cosa è stato aggiunto un link ad un altro post.\n      first_reaction:\n        name:\n          other: Prima Reazione\n        desc:\n          other: Prima reazione al post.\n      first_share:\n        name:\n          other: Prima Condivisione\n        desc:\n          other: Prima condivisione a un post.\n      scholar:\n        name:\n          other: Studioso\n        desc:\n          other: Ha posto una domanda e ha accettato una risposta\n      commentator:\n        name:\n          other: Commentatore\n        desc:\n          other: Lascia 5 commenti.\n      new_user_of_the_month:\n        name:\n          other: Nuovo Utente del Mese\n        desc:\n          other: Contributi straordinari nel primo mese.\n      read_guidelines:\n        name:\n          other: Leggi le Linee Guida\n        desc:\n          other: Leggi le [linee guida della community].\n      reader:\n        name:\n          other: Lettore\n        desc:\n          other: Leggi ogni risposta in un argomento con più di 10 risposte.\n      welcome:\n        name:\n          other: Benvenuto\n        desc:\n          other: Ricevuto un voto positivo.\n      nice_share:\n        name:\n          other: Condivisione positiva.\n        desc:\n          other: Ha condiviso un post con 25 visitatori unici.\n      good_share:\n        name:\n          other: Condivisione positiva.\n        desc:\n          other: Condiviso un post con 300 visitatori unici.\n      great_share:\n        name:\n          other: Grande Condivisione\n        desc:\n          other: Condiviso un post con 1000 visitatori unici.\n      out_of_love:\n        name:\n          other: Fuori Amore\n        desc:\n          other: Usato 50 voti in su in un giorno.\n      higher_love:\n        name:\n          other: Amore Superiore\n        desc:\n          other: Usato 50 voti in su in un giorno 5 volte.\n      crazy_in_love:\n        name:\n          other: Pazzo innamorato\n        desc:\n          other: Utilizzato 50 voti in su in un giorno 20 volte.\n      promoter:\n        name:\n          other: Promotore\n        desc:\n          other: Invitato un utente.\n      campaigner:\n        name:\n          other: Campagnia\n        desc:\n          other: Invitato a 3 utenti di base.\n      champion:\n        name:\n          other: Campione\n        desc:\n          other: Invitati 5 membri.\n      thank_you:\n        name:\n          other: Grazie\n        desc:\n          other: Il post ha ricevuto 20 voti positivi e 10 voti positivi.\n      gives_back:\n        name:\n          other: Feedback\n        desc:\n          other: Post con 100 voti positivi e 100 voti positivi espressi.\n      empathetic:\n        name:\n          other: Empatico\n        desc:\n          other: Ha 500 posti votati e ha rinunciato a 1000 voti.\n      enthusiast:\n        name:\n          other: Entusiasta\n        desc:\n          other: Visitato 10 giorni consecutivi.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visitato 100 giorni consecutivi.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visitato 365 giorni consecutivi.\n      anniversary:\n        name:\n          other: Anniversario\n        desc:\n          other: Membro attivo per un anno, pubblicato almeno una volta.\n      appreciated:\n        name:\n          other: Apprezzato\n        desc:\n          other: Ricevuto 1 voto su 20 posti.\n      respected:\n        name:\n          other: Rispettati\n        desc:\n          other: Ricevuto 2 voto su 100 posti.\n      admired:\n        name:\n          other: Ammirato\n        desc:\n          other: Ricevuto 5 voto su 300 posti.\n      solved:\n        name:\n          other: Risolto\n        desc:\n          other: Avere una risposta accettata.\n      guidance_counsellor:\n        name:\n          other: Consulente Di Orientamento\n        desc:\n          other: Si accettano 10 risposte.\n      know_it_all:\n        name:\n          other: Sa tutto\n        desc:\n          other: Si accettano 50 risposte.\n      solution_institution:\n        name:\n          other: Istituzione Di Soluzione\n        desc:\n          other: Si accettano 150 risposte.\n      nice_answer:\n        name:\n          other: Bella risposta\n        desc:\n          other: Punteggio domande pari o superiore a 10.\n      good_answer:\n        name:\n          other: Buona risposta\n        desc:\n          other: Punteggio domande pari o superiore a 25.\n      great_answer:\n        name:\n          other: Risposta molto buona\n        desc:\n          other: Punteggio domande pari o superiore a 50.\n      nice_question:\n        name:\n          other: Bella domanda\n        desc:\n          other: Punteggio domande pari o superiore a 10.\n      good_question:\n        name:\n          other: Buona domanda\n        desc:\n          other: Punteggio della domanda di 25 o più\n      great_question:\n        name:\n          other: Ottima domanda\n        desc:\n          other: Punteggio domande pari o superiore a 50.\n      popular_question:\n        name:\n          other: Domanda popolare\n        desc:\n          other: \"Domanda con 500 visualizzazioni\\n\"\n      notable_question:\n        name:\n          other: Domanda notevole\n        desc:\n          other: Domanda con 1.000 visualizzazioni.\n      famous_question:\n        name:\n          other: Domanda celebre\n        desc:\n          other: \"Domanda con 5.000 visualizzazioni.\\n.\"\n      popular_link:\n        name:\n          other: Link Popolare\n        desc:\n          other: Pubblicato un link esterno con 50 clic.\n      hot_link:\n        name:\n          other: Link popolare\n        desc:\n          other: Pubblicato un link esterno con 300 clic.\n      famous_link:\n        name:\n          other: Link celebre\n        desc:\n          other: Pubblicato un link esterno con 100 clic.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Primi passi\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Pubblicazione in corso\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Come formattare\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">menzionare un post: <code>#post_id</code></p></li><li><p class=\"mb-2\">per creare collegamenti</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Titolo](https://url.com)</code></pre></li><li><p class=\"mb-2\">mettere ritorni a capo tra i paragrafi</p></li><li><p class=\"mb-2\"><em>corsivo</em> o **<strong>grassetto</strong>**</p></li><li><p class=\"mb-2\">indentare il codice con 4 spazi</p></li><li><p class=\"mb-2\">citare iniziando la riga con <code>&gt;</code></p></li><li><p class=\"mb-2\">utilizzare il backtick per scapeggiare il codice `come _questo_`</code></p></li><li><p class=\"mb-2\">creare recinti di codice con backticks <code>`</code></p><pre class=\"mb-0\"><code>```<br/>codice qui<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Prec\n    next: Successivo\n  page_title:\n    question: Domanda\n    questions: Domande\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Crea tag\n    edit_tag: Modifica Tag\n    ask_a_question: Crea domanda\n    edit_question: Modifica Domanda\n    edit_answer: Modifica risposta\n    search: Cerca\n    posts_containing: Post contenenti\n    settings: Impostazioni\n    notifications: Notifiche\n    login: Accedi\n    sign_up: Registrati\n    account_recovery: Recupero dell'account\n    account_activation: Attivazione account\n    confirm_email: Conferma Email\n    account_suspended: Account sospeso\n    admin: Amministratore\n    change_email: Modifica la tua email\n    install: Installazione di Answer\n    upgrade: Upgrade di Answer\n    maintenance: Manutenzione del sito\n    users: Utenti\n    oauth_callback: Elaborazione in corso\n    http_404: Errore HTTP 404\n    http_50X: Errore HTTP 500\n    http_403: Errore HTTP 403\n    logout: Disconnetti\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifiche\n    inbox: Posta in arrivo\n    achievement: Risultati\n    new_alerts: Nuovi avvisi\n    all_read: Segna tutto come letto\n    show_more: Mostra di più\n    someone: Qualcuno\n    inbox_type:\n      all: Tutti\n      posts: Messaggi\n      invites: Inviti\n      votes: Voti\n    answer: Risposta\n    question: Domanda\n    badge_award: Distintivo\n  suspended:\n    title: Il tuo account è stato sospeso\n    until_time: \"Il tuo account è stato sospeso fino a {{ time }}.\"\n    forever: Questo utente è stato sospeso per sempre.\n    end: Non soddisfi le linee guida della community.\n    contact_us: Contattaci\n  editor:\n    blockquote:\n      text: Citazione\n    bold:\n      text: Forte\n    chart:\n      text: Grafico\n      flow_chart: Diagramma di flusso\n      sequence_diagram: Diagramma di sequenza\n      class_diagram: Diagramma di classe\n      state_diagram: Diagramma di stato\n      entity_relationship_diagram: Diagramma Entità-Relazione\n      user_defined_diagram: \"\\nDiagramma definito dall'utente\"\n      gantt_chart: Diagramma di Gantt\n      pie_chart: Grafico a torta\n    code:\n      text: Esempio di codice\n      add_code: Aggiungi un esempio di codice\n      form:\n        fields:\n          code:\n            label: Codice\n            msg:\n              empty: Il codice non può essere vuoto\n          language:\n            label: Lingua\n            placeholder: Rilevamento automatico\n      btn_cancel: Cancella\n      btn_confirm: Aggiungi\n    formula:\n      text: Formula\n      options:\n        inline: Formula nella linea\n        block: \"\\nFormula di blocco\"\n    heading:\n      text: Intestazione\n      options:\n        h1: Intestazione 1\n        h2: Intestazione 2\n        h3: Intestazione 3\n        h4: Intestazione 4\n        h5: Intestazione 5\n        h6: Intestazione 6\n    help:\n      text: Aiuto\n    hr:\n      text: Riga orizzontale\n    image:\n      text: Immagine\n      add_image: Aggiungi immagine\n      tab_image: Carica immagine\n      form_image:\n        fields:\n          file:\n            label: File d'immagine\n            btn: Scegli immagine\n            msg:\n              empty: Il file non può essere vuoto.\n              only_image: Sono ammesse solo le immagini\n              max_size: La dimensione del file non può superare {{size}} MB.\n          desc:\n            label: Descrizione\n      tab_url: Url dell'Immagine\n      form_url:\n        fields:\n          url:\n            label: URL dell'immagine\n            msg:\n              empty: L'URL dell'immagine non può essere vuoto.\n          name:\n            label: Descrizione\n      btn_cancel: Cancella\n      btn_confirm: Aggiungi\n      uploading: Caricamento in corso...\n    indent:\n      text: Indenta\n    outdent:\n      text: Deindenta\n    italic:\n      text: Corsivo\n    link:\n      text: Collegamento ipertestuale\n      add_link: Aggiungi collegamento\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: L'URL non può essere vuoto\n          name:\n            label: Descrizione\n      btn_cancel: Cancella\n      btn_confirm: Aggiungi\n    ordered_list:\n      text: Elenco numerato\n    unordered_list:\n      text: Elenco puntato\n    table:\n      text: Tabella\n      heading: Intestazione\n      cell: Cella\n    file:\n      text: Allega file\n      not_supported: \"Non supportare quel tipo di file. Riprovare con {{file_type}}.\"\n      max_size: \"Allega la dimensione dei file non può superare {{size}} MB.\"\n  close_modal:\n    title: Sto chiudendo questo post come...\n    btn_cancel: Cancella\n    btn_submit: Invia\n    remark:\n      empty: Non può essere vuoto.\n    msg:\n      empty: Per favore seleziona un motivo\n  report_modal:\n    flag_title: Sto segnalando questo post per riportarlo come...\n    close_title: Sto chiudendo questo post come...\n    review_question_title: Rivedi la domanda\n    review_answer_title: Rivedi la risposta\n    review_comment_title: Rivedi il commento\n    btn_cancel: Cancella\n    btn_submit: Invia\n    remark:\n      empty: Non può essere vuoto.\n    msg:\n      empty: Per favore seleziona un motivo\n      not_a_url: Formato URL errato.\n      url_not_match: L'origine dell'URL non corrisponde al sito web corrente.\n  tag_modal:\n    title: Crea un nuovo tag\n    form:\n      fields:\n        display_name:\n          label: Nome da visualizzare\n          msg:\n            empty: Il nome utente non può essere vuoto.\n            range: Nome utente fino a 35 caratteri.\n        slug_name:\n          label: Slug dell'URL\n          desc: URL slug fino a 35 caratteri.\n          msg:\n            empty: Lo slug dell'URL non può essere vuoto.\n            range: Slug dell'URL fino a 35 caratteri.\n            character: Lo slug dell'URL contiene un set di caratteri non consentiti.\n        desc:\n          label: Descrizione\n        revision:\n          label: Revisione\n        edit_summary:\n          label: Modifica il riepilogo\n          placeholder: >-\n            Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata)\n    btn_cancel: Cancella\n    btn_submit: Invia\n    btn_post: Pubblica un nuovo tag\n  tag_info:\n    created_at: Creato\n    edited_at: Modificato\n    history: Cronologia\n    synonyms:\n      title: Sinonimi\n      text: I seguenti tag verranno rimappati a\n      empty: Nessun sinonimo trovato.\n      btn_add: Aggiungi un sinonimo\n      btn_edit: Modifica\n      btn_save: Salva\n    synonyms_text: I seguenti tag verranno rimappati a\n    delete:\n      title: Elimina questo tag\n      tip_with_posts: >-\n        <p>Non è consentita <strong>l'eliminazione di tag con post</strong>.</p> <p>Rimuovi prima questo tag dai post.</p>\n      tip_with_synonyms: >-\n        <p>Non è consentita <strong>l'eliminazione di tag con sinonimi</strong>.</p> <p>Per favore, rimuovi prima i sinonimi da questo tag.</p>\n      tip: Sei sicuro di voler cancellare?\n      close: Chiudi\n    merge:\n      title: Unisci tag\n      source_tag_title: Cerca tag\n      source_tag_description: Il tag sorgente e i dati associati verranno rimappati al tag di destinazione.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Modifica Tag\n    default_reason: Modifica tag\n    default_first_reason: Aggiungi tag\n    btn_save_edits: Salva modifiche\n    btn_cancel: Cancella\n  dates:\n    long_date: Mese, giorno\n    long_date_with_year: \"Giorno, Mese, Anno\"\n    long_date_with_time: \"MMM D, AAAA [at] HH:mm\"\n    now: ora\n    x_seconds_ago: \"{{count}}s fa\"\n    x_minutes_ago: \"{{count}}m fa\"\n    x_hours_ago: \"{{count}}h fa\"\n    hour: ora\n    day: giorno\n    hours: ore\n    days: giorni\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: cuore\n    smile: sorriso\n    frown: disapprovare\n    btn_label: aggiungere o rimuovere le reazioni\n    undo_emoji: annulla reazione {{ emoji }}\n    react_emoji: reagire con {{ emoji }}\n    unreact_emoji: non reagire con {{ emoji }}\n  comment:\n    btn_add_comment: Aggiungi un commento\n    reply_to: Rispondi a\n    btn_reply: Rispondi\n    btn_edit: Modifica\n    btn_delete: Cancella\n    btn_flag: Segnala\n    btn_save_edits: Salva modifiche\n    btn_cancel: Cancella\n    show_more: \"{{count}} altri commenti\"\n    tip_question: >-\n      Usa i commenti per chiedere maggiori informazioni o per suggerire miglioramenti. Evita di rispondere alle domande nei commenti.\n    tip_answer: >-\n      Utilizza i commenti per rispondere ad altri utenti o avvisarli delle modifiche. Se stai aggiungendo nuove informazioni, modifica il tuo post invece di commentare.\n    tip_vote: Aggiunge qualcosa di utile al post\n  edit_answer:\n    title: Modifica risposta\n    default_reason: Modifica risposta\n    default_first_reason: Aggiungi risposta\n    form:\n      fields:\n        revision:\n          label: Revisione\n        answer:\n          label: Risposta\n          feedback:\n            characters: Il testo deve contenere almeno 6 caratteri.\n        edit_summary:\n          label: Modifica il riepilogo\n          placeholder: >-\n            Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata)\n    btn_save_edits: Salva modifiche\n    btn_cancel: Annulla\n  tags:\n    title: Tag\n    sort_buttons:\n      popular: Popolari\n      name: Nome\n      newest: Più recente\n    button_follow: Segui\n    button_following: Segui già\n    tag_label: domande\n    search_placeholder: Filtra per nome del tag\n    no_desc: Il tag non ha descrizioni.\n    more: Altro\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Modifica Domanda\n    default_reason: Modifica domanda\n    default_first_reason: Create question\n    similar_questions: Domande simili\n    form:\n      fields:\n        revision:\n          label: Revisione\n        title:\n          label: Titolo\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Il titolo non può essere vuoto.\n            range: Il titolo non può superare i 150 caratteri\n        body:\n          label: Contenuto\n          msg:\n            empty: Il corpo del testo non può essere vuoto.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: I tag non possono essere vuoti.\n        answer:\n          label: Risposta\n          msg:\n            empty: La risposta non può essere vuota.\n        edit_summary:\n          label: Modifica riepilogo\n          placeholder: >-\n            Spiega brevemente le tue modifiche (ortografia corretta, grammatica corretta, formattazione migliorata)\n    btn_post_question: Posta la tua domanda\n    btn_save_edits: Salva modifiche\n    answer_question: Rispondi alla tua domanda\n    post_question&answer: Posta la tua domanda e risposta\n  tag_selector:\n    add_btn: Aggiungi tag\n    create_btn: Crea un nuovo tag\n    search_tag: Cerca tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Nessun tag corrispondente\n    tag_required_text: Tag richiesto (almeno uno)\n  header:\n    nav:\n      question: Domande\n      tag: Tags\n      user: Utenti\n      badges: Badges\n      profile: Profilo\n      setting: Impostazioni\n      logout: Disconnetti\n      admin: Amministratore\n      review: Revisione\n      bookmark: Segnalibri\n      moderation: Moderazione\n    search:\n      placeholder: Cerca\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Modifica\n    loading: caricamento in corso...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Digita il testo sopra\n    msg:\n      empty: Il Captcha non può essere vuoto.\n  inactive:\n    first: >-\n      Hai quasi finito! Abbiamo inviato un'e-mail di attivazione a <bold>{{mail}}</bold>. Segui le istruzioni contenute nella mail per attivare il tuo account.\n    info: \"Se non arriva, controlla la cartella spam.\"\n    another: >-\n      Ti abbiamo inviato un'altra email di attivazione all'indirizzo <bold>{{mail}}</bold>. Potrebbero volerci alcuni minuti prima che arrivi; assicurati di controllare la cartella spam.\n    btn_name: Reinvia l'e-mail di attivazione\n    change_btn_name: Modifica e-mail\n    msg:\n      empty: Non può essere vuoto.\n    resend_email:\n      url_label: Sei sicuro di voler inviare nuovamente l'e-mail di attivazione?\n      url_text: Puoi anche fornire all'utente il link di attivazione riportato sopra.\n  login:\n    login_to_continue: Accedi per continuare\n    info_sign: Non hai un account? <1>Iscriviti</1>\n    info_login: Hai già un account? <1>Accedi</1>\n    agreements: Registrandoti, accetti l'<1>informativa sulla privacy</1> e i <3>termini del servizio</3>.\n    forgot_pass: Password dimenticata?\n    name:\n      label: Nome\n      msg:\n        empty: Il nome non può essere vuoto.\n        range: Il nome deve essere di lunghezza compresa tra 2 e 30 caratteri.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-mail\n      msg:\n        empty: L'email non può essere vuota.\n    password:\n      label: Password\n      msg:\n        empty: La password non può essere vuota.\n        different: Le password inserite su entrambi i lati non corrispondono\n  account_forgot:\n    page_title: Password dimenticata?\n    btn_name: Inviami email di recupero\n    send_success: >-\n      Se un account corrisponde a <strong>{{mail}}</strong>, a breve dovresti ricevere un'e-mail con le istruzioni su come reimpostare la password.\n    email:\n      label: E-mail\n      msg:\n        empty: Il campo email non può essere vuoto.\n  change_email:\n    btn_cancel: Cancella\n    btn_update: Aggiorna indirizzo email\n    send_success: >-\n      Se un account corrisponde a <strong>{{mail}}</strong>, a breve dovresti ricevere un'email con le istruzioni su come reimpostare la password.\n    email:\n      label: Nuova email\n      msg:\n        empty: L'email non può essere vuota.\n  oauth:\n    connect: Connettiti con {{ auth_name }}\n    remove: Rimuovi {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Aggiungi un'email di recupero al tuo account.\n    btn_update: Aggiorna l'indirizzo email\n    email:\n      label: E-mail\n      msg:\n        empty: L'email non può essere vuota.\n    modal_title: Email già esistente\n    modal_content: Questo indirizzo email è già registrato. Sei sicuro che vuoi connetterti all'account esistente?\n    modal_cancel: Cambia email\n    modal_confirm: Connettiti all'account esistente\n  password_reset:\n    page_title: Reimposta la password\n    btn_name: Reimposta la mia password\n    reset_success: >-\n      Hai cambiato con successo la tua password; sarai reindirizzato alla pagina di accesso.\n    link_invalid: >-\n      Siamo spiacenti, questo link di reset della password non è più valido. Forse la password è già stata reimpostata?\n    to_login: Continua per effettuare il login\n    password:\n      label: Password\n      msg:\n        empty: La password non può essere vuota\n        length: La lunghezza deve essere compresa tra 8 e 32\n        different: Le password inserite non corrispondono\n    password_confirm:\n      label: Conferma la nuova password\n  settings:\n    page_title: Impostazioni\n    goto_modify: Vai alle modifiche\n    nav:\n      profile: Profilo\n      notification: Notifiche\n      account: Profilo\n      interface: Interfaccia\n    profile:\n      heading: Profilo\n      btn_name: Salva\n      display_name:\n        label: Visualizza nome\n        msg: Il nome utente non può essere vuoto.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Nome utente\n        caption: Gli altri utenti possono menzionarti con @{{username}}.\n        msg: Il nome utente non può essere vuoto.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Immagine del profilo\n        gravatar: Gravatar\n        gravatar_text: Puoi cambiare l'immagine\n        custom: Personalizzato\n        custom_text: Puoi caricare la tua immagine.\n        default: Sistema\n        msg: Per favore carica un avatar\n      bio:\n        label: Chi sono\n      website:\n        label: Sito web\n        placeholder: \"https://esempio.com\"\n        msg: Formato non corretto del sito web\n      location:\n        label: Luogo\n        placeholder: \"Città, Paese\"\n    notification:\n      heading: Notifiche email\n      turn_on: Accendi\n      inbox:\n        label: Notifiche in arrivo\n        description: Risposte alle tue domande, commenti, inviti e altro ancora.\n      all_new_question:\n        label: Tutte le nuove domande\n        description: Ricevi una notifica per tutte le nuove domande. Fino a 50 domande a settimana.\n      all_new_question_for_following_tags:\n        label: Tutte le nuove domande per i seguenti tag\n        description: Ricevi una notifica delle nuove domande per i seguenti tag.\n    account:\n      heading: Profilo\n      change_email_btn: Modifica e-mail\n      change_pass_btn: Modifica password\n      change_email_info: >-\n        Abbiamo inviato una mail a quell'indirizzo. Si prega di seguire le istruzioni di conferma.\n      email:\n        label: E-mail\n      new_email:\n        label: Nuova mail\n        msg: La nuova email non può essere vuota.\n      pass:\n        label: Password attuale\n        msg: La password non può essere vuota\n      password_title: Password\n      current_pass:\n        label: Password attuale\n        msg:\n          empty: La password attuale non può essere vuota.\n          length: La lunghezza deve essere compresa tra 8 e 32.\n          different: Le due password inserite non corrispondono.\n      new_pass:\n        label: Nuova password\n      pass_confirm:\n        label: Conferma la nuova password\n    interface:\n      heading: Interfaccia\n      lang:\n        label: Lingua dell'interfaccia\n        text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina.\n    my_logins:\n      title: I miei login\n      label: Accedi o registrati su questo sito utilizzando questi account.\n      modal_title: Rimuovi login\n      modal_content: Sei sicuro di voler rimuovere questo login dal tuo account?\n      modal_confirm_btn: Rimuovi\n      remove_success: Rimosso con successo\n  toast:\n    update: Aggiornamento riuscito\n    update_password: Password modificata con successo.\n    flag_success: Grazie per la segnalazione.\n    forbidden_operate_self: Vietato operare su se stessi.\n    review: Le tue modifiche verranno visualizzata dopo la revisione.\n    sent_success: Inviato correttamente\n  related_question:\n    title: Related\n    answers: risposte\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Persone Interpellate\n    desc: Seleziona le persone che pensi potrebbero conoscere la risposta.\n    invite: Invita a rispondere\n    add: Aggiungi contatti\n    search: Cerca persone\n  question_detail:\n    action: Azione\n    created: Created\n    Asked: Chiesto\n    asked: chiesto\n    update: Modificato\n    Edited: Edited\n    edit: modificato\n    commented: commentato\n    Views: Visualizzati\n    Follow: Segui\n    Following: Segui già\n    follow_tip: Segui questa domanda per ricevere notifiche\n    answered: Risposto\n    closed_in: Chiuso in\n    show_exist: Mostra domanda esistente.\n    useful: Utile\n    question_useful: È utile e chiaro\n    question_un_useful: Non è chiaro né utile\n    question_bookmark: Aggiungi questa domanda ai segnalibri\n    answer_useful: È utile\n    answer_un_useful: Non è utile\n    answers:\n      title: Risposte\n      score: Punteggio\n      newest: Più recenti\n      oldest: Meno recente\n      btn_accept: Accetta\n      btn_accepted: Accettato\n    write_answer:\n      title: La tua risposta\n      edit_answer: Modifica la mia risposta attuale\n      btn_name: Pubblica la tua risposta\n      add_another_answer: Aggiungi un'altra risposta\n      confirm_title: Continua a rispondere\n      continue: Continua\n      confirm_info: >-\n        <p>Sei sicuro di voler aggiungere un'altra risposta?</p><p>In alternativa, puoi usare il link di modifica per perfezionare e migliorare la tua risposta esistente.</p>\n      empty: La risposta non può essere vuota.\n      characters: Il contenuto deve avere una lunghezza di almeno 6 caratteri.\n      tips:\n        header_1: Grazie per la risposta\n        li1_1: Assicurati di <strong>rispondere alla domanda</strong>. Fornisci dettagli e condividi la tua ricerca.\n        li1_2: Effettua il backup di qualsiasi dichiarazione fatta con riferimenti o esperienze personali.\n        header_2: Ma <strong>evita</strong>...\n        li2_1: Chiedere aiuto, cercare chiarimenti o rispondere ad altre risposte.\n    reopen:\n      confirm_btn: Riapri\n      title: Riapri questo post\n      content: Sei sicuro di voler riaprire?\n    list:\n      confirm_btn: Listare\n      title: Lista questo post\n      content: Sei sicuro di volerlo listare?\n    unlist:\n      confirm_btn: Rimuovi dall'elenco\n      title: Rimuovi questo post\n      content: Sei sicuro di voler rimuovere dall'elenco?\n    pin:\n      title: Fissa questo post in cima al profilo\n      content: Sei sicuro di voler fissare in blocco? Questo post apparirà in cima a tutte le liste.\n      confirm_btn: Fissa sul profilo\n  delete:\n    title: Cancella questo post\n    question: >-\n      Non consigliamo di eliminare<strong>domande con risposte</strong> perché ciò priva i futuri lettori di questa conoscenza.</p><p>L'eliminazione ripetuta di domande con risposte può comportare il blocco delle domande nel tuo account. Sei sicuro di voler eliminare?\n    answer_accepted: >-\n      <p>Non consigliamo di <strong>eliminare la risposta accettata</strong> perché così facendo si priva i futuri lettori di questa conoscenza. </p> La cancellazione ripetuta delle risposte accettate può causare il blocco del tuo account dalla risposta. Sei sicuro di voler eliminare?\n    other: Sei sicuro di voler eliminare?\n    tip_answer_deleted: Questa risposta è stata cancellata\n    undelete_title: Ripristina questo post\n    undelete_desc: Sei sicuro di voler ripristinare?\n  btns:\n    confirm: Conferma\n    cancel: Cancella\n    edit: Modifica\n    save: Salva\n    delete: Elimina\n    undelete: Ripristina\n    list: Aggiungi all'elenco\n    unlist: Rimuovi dall'elenco\n    unlisted: Rimosso dall'elenco\n    login: Accedi\n    signup: Registrati\n    logout: Disconnetti\n    verify: Verifica\n    create: Create\n    approve: Approva\n    reject: Rifiuta\n    skip: Salta\n    discard_draft: Elimina bozza\n    pinned: Fissato in cima\n    all: Tutti\n    question: Domanda\n    answer: Risposta\n    comment: Commento\n    refresh: Aggiorna\n    resend: Rinvia\n    deactivate: Disattivare\n    active: Attivo\n    suspend: Sospendi\n    unsuspend: Riabilita\n    close: Chiudi\n    reopen: Riapri\n    ok: OK\n    light: Illuminazione\n    dark: Scuro\n    system_setting: Configurazione di sistema\n    default: Predefinito\n    reset: Resetta\n    tag: Tag\n    post_lowercase: post\n    filter: '﻿Filtra'\n    ignore: Ignora\n    submit: Invia\n    normal: Normale\n    closed: Chiuso\n    deleted: Eliminato\n    deleted_permanently: Deleted permanently\n    pending: In attesa\n    more: Altro\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Risultati della ricerca\n    keywords: Parole chiave\n    options: Opzioni\n    follow: Segui\n    following: Segui già\n    counts: \"{{count}} Risultati\"\n    counts_loading: \"... Results\"\n    more: Altro\n    sort_btns:\n      relevance: Rilevanza\n      newest: Più recenti\n      active: Attivo\n      score: Punteggio\n      more: Altro\n    tips:\n      title: Suggerimenti per ricerca avanzata\n      tag: \"<1>[tag]</1> cerca dentro un tag\"\n      user: \"<1>user:username</1> ricerca per autore\"\n      answer: \"<1>risposte:0</1> domande senza risposta\"\n      score: \"<1>punteggio:3</1> messaggi con un punteggio di 3+\"\n      question: \"<1>is:question</1> cerca domande\"\n      is_answer: \"<1>is:answer</1> cerca risposte\"\n    empty: Non siamo riusciti a trovare nulla. <br /> Prova parole chiave diverse o meno specifiche.\n  share:\n    name: Condividi\n    copy: Copia il link\n    via: Condividi il post via...\n    copied: Copiato\n    facebook: Condividi su Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Non puoi votare un tuo post!\n  modal_confirm:\n    title: Errore...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Il tuo nuovo account è confermato; sarai reindirizzato alla home page.\n    link: Continua alla Homepage\n    oops: Oops!\n    invalid: Il link che hai usato non è più attivo.\n    confirm_new_email: La tua email è stata aggiornata.\n    confirm_new_email_invalid: >-\n      Siamo spiacenti, questo link di conferma non è più valido. Forse la tua email è già stata modificata?\n  unsubscribe:\n    page_title: Annulla l'iscrizione\n    success_title: Cancellazione effettuata con successo\n    success_desc: Sei stato rimosso con successo da questa lista e non riceverai ulteriori email\n    link: Cambia impostazioni\n  question:\n    following_tags: Tag seguenti\n    edit: Modifica\n    save: Salva\n    follow_tag_tip: Segui i tag per curare la tua lista di domande.\n    hot_questions: Domande scottanti\n    all_questions: Tutte le domande\n    x_questions: \"{{ count }} Domande\"\n    x_answers: \"{{count}} risposte\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Domande\n    answers: Risposte\n    newest: Più recenti\n    active: Attivo\n    hot: Caldo\n    frequent: Frequenti\n    recommend: Raccomandato\n    score: Punteggio\n    unanswered: Senza risposta\n    modified: Modificato\n    answered: Risposte\n    asked: chiesto\n    closed: Chiuso\n    follow_a_tag: Segui un tag\n    more: Altro\n  personal:\n    overview: Informazioni Generali\n    answers: Risposte\n    answer: Risposta\n    questions: Domande\n    question: Domanda\n    bookmarks: Segnalibri\n    reputation: Reputazione\n    comments: Commenti\n    votes: Voti\n    badges: Badges\n    newest: Più recenti\n    score: Punteggio\n    edit_profile: Modifica profilo\n    visited_x_days: \"{{ count }} giorni visitati\"\n    viewed: Visualizzati\n    joined: Iscritto\n    comma: \",\"\n    last_login: Visto\n    about_me: Chi sono\n    about_me_empty: \"// Ciao, mondo !\"\n    top_answers: Migliori risposte\n    top_questions: Domande principali\n    stats: Statistiche\n    list_empty: Nessun post trovato.<br />Forse desideri selezionare una scheda diversa?\n    content_empty: Nessun post trovato.\n    accepted: Accettato\n    answered: risposto\n    asked: chiesto\n    downvoted: votato negativamente\n    mod_short: Moderatore\n    mod_long: Moderatori\n    x_reputation: reputazione\n    x_votes: voti ricevuti\n    x_answers: risposte\n    x_questions: domande\n    recent_badges: Badges Recenti\n  install:\n    title: Installazione\n    next: Avanti\n    done: Fatto\n    config_yaml_error: Impossibile creare il file config.yaml.\n    lang:\n      label: Scegli una lingua\n    db_type:\n      label: Motore database\n    db_username:\n      label: Nome utente\n      placeholder: root\n      msg: Il nome utente non può essere vuoto.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: La password non può essere vuota.\n    db_host:\n      label: Host del database\n      placeholder: \"db:3306\"\n      msg: L'host del database non può essere vuoto.\n    db_name:\n      label: Nome database\n      placeholder: risposta\n      msg: Il nome del database non può essere vuoto.\n    db_file:\n      label: File del database\n      placeholder: /data/answer.db\n      msg: Il file del database non può essere vuoto.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Crea config.yaml\n      label: File config.yaml creato.\n      desc: >-\n        Puoi creare manualmente il file <1>config.yaml</1> nella directory <1>/var/wwww/xxx/</1> e incollarvi il seguente testo.\n      info: Una volta fatto, fai clic sul pulsante \"Avanti\".\n    site_information: Informazioni sul sito\n    admin_account: Account Amministratore\n    site_name:\n      label: Nome del sito\n      msg: Il nome del sito non può essere vuoto.\n      msg_max_length: Il nome del sito deve contenere un massimo di 30 caratteri.\n    site_url:\n      label: URL del sito\n      text: L'indirizzo del tuo sito.\n      msg:\n        empty: L'URL del sito non può essere vuoto.\n        incorrect: Formato errato dell'URL del sito.\n        max_length: L'URL del sito deve contenere un massimo di 512 caratteri.\n    contact_email:\n      label: Email di contatto\n      text: Indirizzo e-mail del contatto chiave responsabile di questo sito.\n      msg:\n        empty: L'email del contatto non può essere vuota.\n        incorrect: Formato errato dell'e-mail di contatto.\n    login_required:\n      label: Privato\n      switch: Login obbligatorio\n      text: Solo gli utenti registrati possono accedere a questa community.\n    admin_name:\n      label: Nome\n      msg: Il nome non può essere vuoto.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        Avrai bisogno di questa password per accedere. Conservala in un luogo sicuro.\n      msg: La password non può essere vuota\n      msg_min_length: La password deve contenere almeno 8 caratteri.\n      msg_max_length: La password deve contenere un massimo di 32 caratteri.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: E-mail\n      text: Avrai bisogno di questa email per accedere.\n      msg:\n        empty: Il campo email non può essere vuoto.\n        incorrect: Formato dell'email errato.\n    ready_title: Il tuo sito è pronto\n    ready_desc: >-\n      Se vuoi cambiare più impostazioni, visita la sezione <1>admin</1> che si trova nel menu del sito.\n    good_luck: \"Divertiti e buona fortuna!\"\n    warn_title: Pericolo\n    warn_desc: >-\n      Il file <1>config.yaml</1> esiste già. Se vuoi reimpostare uno qualsiasi degli elementi di configurazione in questo file, eliminalo prima.\n    install_now: Puoi provare a <1>installare ora</1>.\n    installed: Già installato\n    installed_desc: >-\n      Sembra che tu abbia già installato. Per reinstallare, cancella prima le vecchie tabelle del database.\n    db_failed: Connessione al database fallita\n    db_failed_desc: >-\n      Questo significa che le informazioni sul database nel file <1>config.yaml</1> non sono corrette o che non è stato possibile stabilire il contatto con il server del database. Ciò potrebbe significare che il server del database del tuo host è inattivo.\n  counts:\n    views: visualizzazioni\n    votes: Voti\n    answers: risposte\n    accepted: Accettato\n  page_error:\n    http_error: Errore HTTP {{ code }}\n    desc_403: Non hai i permessi per accedere a questa pagina.\n    desc_404: Sfortunatamente, questa pagina non esiste.\n    desc_50X: Il server ha riscontrato un errore e non è stato possibile completare la richiesta.\n    back_home: Torna alla home page\n  page_maintenance:\n    desc: \"Siamo in manutenzione, torneremo presto.\"\n  nav_menus:\n    dashboard: Pannello di controllo\n    contents: Contenuti\n    questions: Domande\n    answers: Risposte\n    users: Utenti\n    badges: Badges\n    flags: Contrassegni\n    settings: Impostazioni\n    general: Generale\n    interface: Interfaccia\n    smtp: Protocollo di Trasferimento Posta Semplice\n    branding: Marchio\n    legal: Legale\n    write: Scrivi\n    terms: Terms\n    tos: Termini del servizio\n    privacy: Privacy\n    seo: SEO\n    customize: Personalizza\n    themes: Temi\n    login: Accedi\n    privileges: Privilegi\n    plugins: Plugin\n    installed_plugins: Plugin installati\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Benvenuto/a su {{site_name}}!\n  user_center:\n    login: Accedi\n    qrcode_login_tip: Si prega di utilizzare {{ agentName }} per scansionare il codice QR e accedere.\n    login_failed_email_tip: Accesso non riuscito. Consenti a questa app di accedere alle tue informazioni email prima di riprovare.\n  badges:\n    modal:\n      title: Congratulazioni\n      content: Hai guadagnato un nuovo distintivo.\n      close: Chiudi\n      confirm: Visualizza badge\n    title: Badges\n    awarded: Premiati\n    earned_×: Ottenuto ×{{ number }}\n    ×_awarded: \"{{ number }} premiato\"\n    can_earn_multiple: Puoi guadagnare questo più volte.\n    earned: Ottenuti\n  admin:\n    admin_header:\n      title: Amministratore\n    dashboard:\n      title: Pannello di controllo\n      welcome: Benvenuto ad Admin!\n      site_statistics: Statistiche del sito\n      questions: \"Domande:\"\n      resolved: \"Risolto:\"\n      unanswered: \"Senza risposta:\"\n      answers: \"Risposte:\"\n      comments: \"Commenti:\"\n      votes: \"Voti:\"\n      users: \"Utenti:\"\n      flags: \"Flags\"\n      reviews: \"Revisioni\"\n      site_health: Stato del sito\n      version: \"Versione:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Carica Cartella\"\n      run_mode: \"Modalità di esecuzione:\"\n      private: Privato\n      public: Pubblico\n      smtp: \"Protocollo di Trasferimento Posta Semplice\"\n      timezone: \"Fuso orario:\"\n      system_info: Info sistema\n      go_version: \"Versione Go:\"\n      database: \"Banca dati:\"\n      database_size: \"Dimensioni del database\"\n      storage_used: \"Spazio di archiviazione utilizzato:\"\n      uptime: \"Tempo di attività:\"\n      links: Collegamenti\n      plugins: Plugin\n      github: GitHub\n      blog: Blog\n      contact: Contatti\n      forum: Forum\n      documents: Documenti\n      feedback: Feedback\n      support: Assistenza\n      review: Revisione\n      config: Configurazione\n      update_to: Aggiornato a\n      latest: Recenti\n      check_failed: Controllo fallito\n      \"yes\": \"Sì\"\n      \"no\": \"No\"\n      not_allowed: Non autorizzato\n      allowed: Consentito\n      enabled: Abilitato\n      disabled: Disabilitato\n      writable: Editabile\n      not_writable: Non editabile\n    flags:\n      title: Contrassegni\n      pending: In attesa\n      completed: Completato\n      flagged: Contrassegnato\n      flagged_type: Contrassegnato {{ type }}\n      created: Creato\n      action: Azione\n      review: Revisione\n    user_role_modal:\n      title: Cambia ruolo utente in...\n      btn_cancel: Cancella\n      btn_submit: Invia\n    new_password_modal:\n      title: Imposta una nuova password\n      form:\n        fields:\n          password:\n            label: Password\n            text: L'utente sarà disconnesso e dovrà effettuare nuovamente il login.\n            msg: La password deve contenere da 8 a 32 caratteri.\n      btn_cancel: Cancella\n      btn_submit: Invia\n    edit_profile_modal:\n      title: Modifica profilo\n      form:\n        fields:\n          display_name:\n            label: Visualizza nome\n            msg_range: Il nome visualizzato deve essere di 2-30 caratteri di lunghezza.\n          username:\n            label: Nome utente\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Indirizzo e-mail non valido.\n      edit_success: Modificato con successo\n      btn_cancel: Annulla\n      btn_submit: Invia\n    user_modal:\n      title: Aggiungi un nuovo utente\n      form:\n        fields:\n          users:\n            label: Aggiungi utenti in blocco\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separare “nome, email, password” con delle virgole. Un utente per riga.\n            msg: \"Inserisci l'email dell'utente, una per riga.\"\n          display_name:\n            label: Nome da visualizzare\n            msg: Il nome visualizzato deve essere di 2-30 caratteri di lunghezza.\n          email:\n            label: E-mail\n            msg: L'email non è valida.\n          password:\n            label: Password\n            msg: La password deve contenere da 8 a 32 caratteri.\n      btn_cancel: Cancella\n      btn_submit: Invia\n    users:\n      title: Utenti\n      name: Nome\n      email: E-mail\n      reputation: Reputazione\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Sospendi fino al\n      status: Stato\n      role: Ruolo\n      action: Azione\n      change: Modifica\n      all: Tutti\n      staff: Personale\n      more: Altro\n      inactive: Inattivo\n      suspended: Sospeso\n      deleted: Eliminato\n      normal: Normale\n      Moderator: Moderatore\n      Admin: Amministratore\n      User: Utente\n      filter:\n        placeholder: \"Filtra per nome, utente:id\"\n      set_new_password: Imposta una nuova password\n      edit_profile: Modifica profilo\n      change_status: Modifica lo stato\n      change_role: Cambia il ruolo\n      show_logs: Visualizza i log\n      add_user: Aggiungi utente\n      deactivate_user:\n        title: Disattiva utente\n        content: Un utente inattivo deve riconvalidare la propria email.\n      delete_user:\n        title: Rimuovi questo utente\n        content: Sei sicuro di voler eliminare questo utente? L'operazione è permanente.\n        remove: Rimuovi il loro contenuto\n        label: Rimuovi tutte le domande, risposte, commenti, ecc.\n        text: Non selezionare questa opzione se desideri eliminare solo l'account dell'utente.\n      suspend_user:\n        title: Sospendi questo utente\n        content: Un utente sospeso non può accedere.\n        label: Per quanto tempo vuoi sospendere l'utente?\n        forever: Per sempre\n    questions:\n      page_title: Domande\n      unlisted: Rimosso dall'elenco\n      post: Posta\n      votes: Voti\n      answers: Risposte\n      created: Creato\n      status: Stato\n      action: Azione\n      change: Modifica\n      pending: In attesa\n      filter:\n        placeholder: \"Filtra per titolo, domanda:id\"\n    answers:\n      page_title: Risposte\n      post: Post\n      votes: Voti\n      created: Creato\n      status: Stato\n      action: Azione\n      change: Cambio\n      filter:\n        placeholder: \"Filtra per titolo, domanda:id\"\n    general:\n      page_title: Generale\n      name:\n        label: Nome del sito\n        msg: Il nome del sito non può essere vuoto.\n        text: \"Il nome di questo sito, come usato nel tag del titolo.\"\n      site_url:\n        label: URL del sito\n        msg: L'url del sito non può essere vuoto.\n        validate: Inserisci un URL valido.\n        text: L'indirizzo del tuo sito.\n      short_desc:\n        label: Descrizione breve del sito\n        msg: La descrizione breve del sito non può essere vuota.\n        text: \"Breve descrizione, come utilizzata nel tag del titolo sulla home page.\"\n      desc:\n        label: Descrizione del sito\n        msg: La descrizione del sito non può essere vuota.\n        text: \"Descrivi questo sito in una frase, come utilizzato nel tag meta description.\"\n      contact_email:\n        label: Email di contatto\n        msg: L'email del contatto non può essere vuota.\n        validate: Email di contatto non valida.\n        text: Indirizzo e-mail del contatto chiave responsabile di questo sito.\n      check_update:\n        label: Aggiornamenti Software\n        text: Controlla automaticamente gli aggiornamenti\n    interface:\n      page_title: Interfaccia\n      language:\n        label: Lingua dell'interfaccia\n        msg: La lingua dell'interfaccia non può essere vuota.\n        text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina.\n      time_zone:\n        label: Fuso orario\n        msg: Il fuso orario non può essere vuoto.\n        text: Scegli una città con il tuo stesso fuso orario.\n      avatar:\n        label: Avatar Predefinito\n        text: Per gli utenti senza un proprio avatar personalizzato.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: Protocollo di Trasferimento Posta Semplice\n      from_email:\n        label: Dall'email\n        msg: L'email del contatto non può essere vuota.\n        text: L'indirizzo email da cui vengono inviate le email.\n      from_name:\n        label: Dal nome\n        msg: \"Il nome del mittente non può essere vuoto.\\n\"\n        text: Il nome da cui vengono inviate le email.\n      smtp_host:\n        label: Host SMTP\n        msg: L'host SMTP non può essere vuoto.\n        text: Il tuo server di posta.\n      encryption:\n        label: Crittografia\n        msg: La crittografia non può essere vuota.\n        text: Per la maggior parte dei server SSL è l'opzione consigliata.\n        ssl: SSL\n        tls: TLS\n        none: Nessuna\n      smtp_port:\n        label: Porta SMTP\n        msg: La porta SMTP deve essere numero 1 ~ 65535.\n        text: La porta del tuo server di posta.\n      smtp_username:\n        label: Nome utente SMTP\n        msg: Il nome utente SMTP non può essere vuoto.\n      smtp_password:\n        label: Password SMTP\n        msg: La password SMTP non può essere vuota.\n      test_email_recipient:\n        label: Verifica destinatari email\n        text: Fornisci l'indirizzo email che riceverà i test inviati.\n        msg: Destinatari email di prova non validi\n      smtp_authentication:\n        label: \"\\nAbilita l'autenticazione\"\n        title: Autenticazione SMTP\n        msg: L'autenticazione SMTP non può essere vuota.\n        \"yes\": \"Sì\"\n        \"no\": \"No\"\n    branding:\n      page_title: Marchio\n      logo:\n        label: Logo\n        msg: Il logo non può essere vuoto.\n        text: L'immagine del logo in alto a sinistra del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56 e proporzioni superiori a 3:1. Se lasciato vuoto, il testo del titolo del sito verrà mostrato.\n      mobile_logo:\n        label: Logo per versione mobile\n        text: \"\\nIl logo utilizzato nella versione mobile del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56. Se lasciata vuota, verrà utilizzata l'immagine dell'impostazione \\\"logo\\\".\"\n      square_icon:\n        label: Icona quadrata\n        msg: L'icona quadrata non può essere vuota.\n        text: \"Immagine utilizzata come base per le icone dei metadata. Idealmente dovrebbe essere più grande di 512x512.\\n\"\n      favicon:\n        label: Favicon\n        text: Una icona favorita per il tuo sito. Per funzionare correttamente su un CDN deve essere un png. Verrà ridimensionato a 32x32. Se lasciato vuoto, verrà utilizzata l'\"icona quadrata\".\n    legal:\n      page_title: Legale\n      terms_of_service:\n        label: Termini del servizio\n        text: \"Puoi aggiungere qui i termini del contenuto del servizio. Se hai già un documento ospitato altrove, fornisci qui l'URL completo.\"\n      privacy_policy:\n        label: Informativa sulla privacy\n        text: \"Puoi aggiungere il contenuto della politica sulla privacy qui. Se hai già un documento ospitato altrove, fornisci l'URL completo qui.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Risposta a scrivere\n        label: Ogni utente può scrivere una sola risposta per ogni domanda\n        text: \"Disattiva per consentire agli utenti di scrivere risposte multiple alla stessa domanda, il che potrebbe causare una risposta sfocata.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Raccomanda tag\n        text: \"I tag consigliati verranno mostrati nell'elenco a discesa per impostazione predefinita.\"\n        msg:\n          contain_reserved: \"i tag consigliati non possono contenere tag riservati\"\n      required_tag:\n        title: Imposta tag necessari\n        label: Imposta “Raccomanda tag” come tag richiesti\n        text: \"Ogni nuova domanda deve avere almeno un tag raccomandato.\"\n      reserved_tags:\n        label: Tag riservati\n        text: \"I tag riservati possono essere utilizzati solo dal moderatore.\"\n      image_size:\n        label: Dimensione massima dell'immagine (MB)\n        text: \"La dimensione massima del caricamento dell'immagine.\"\n      attachment_size:\n        label: Dimensione massima degli allegati (MB)\n        text: \"Dimensione massima del caricamento dei file allegati.\"\n      image_megapixels:\n        label: Massima immagine megapixel\n        text: \"Numero massimo di megapixel consentiti per un'immagine.\"\n      image_extensions:\n        label: Estensioni di immagini autorizzate\n        text: \"Un elenco di estensioni di file consentite per la visualizzazione dell'immagine, separate da virgole.\"\n      attachment_extensions:\n        label: Estensioni di allegati autorizzate\n        text: \"Una lista di estensioni di file consentite per il caricamento, separate con virgole. ATTENZIONE: Consentire i caricamenti potrebbe causare problemi di sicurezza.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Le strutture URL personalizzate possono migliorare l'usabilità e la compatibilità futura dei tuoi link.\n      robots:\n        label: robots.txt\n        text: Questo sovrascriverà definitivamente tutte le impostazioni relative al sito.\n    themes:\n      page_title: Temi\n      themes:\n        label: Temi\n        text: Seleziona un tema esistente.\n      color_scheme:\n        label: Schema colore\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Colore primario\n        text: Modifica i colori utilizzati dai tuoi temi\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS e HTML\n      custom_css:\n        label: CSS personalizzato\n        text: >\n\n      head:\n        label: Testata\n        text: >\n\n      header:\n        label: Intestazione\n        text: >\n\n      footer:\n        label: Piè di pagina\n        text: Questo verrà inserito prima di &lt;/body>.\n      sidebar:\n        label: Barra laterale\n        text: Questo verrà inserito nella barra laterale.\n    login:\n      page_title: Accedi\n      membership:\n        title: Adesione\n        label: Consenti nuove registrazioni\n        text: Disattiva per impedire a chiunque di creare un nuovo account.\n      email_registration:\n        title: Registrazione email\n        label: Consenti registrazione email\n        text: Disattiva per impedire a chiunque di creare un nuovo account tramite email.\n      allowed_email_domains:\n        title: Domini email consentiti\n        text: \"Domini email con cui gli utenti devono registrare gli account. Un dominio per riga. Verrà ignorato quando vuoto.\\n\"\n      private:\n        title: Privato\n        label: Login obbligatorio\n        text: Solo gli utenti registrati possono accedere a questa community.\n      password_login:\n        title: Password di accesso\n        label: Consenti il login di email e password\n        text: \"ATTENZIONE: Se disattivi, potresti non essere in grado di accedere se non hai precedentemente configurato un altro metodo di login.\"\n    installed_plugins:\n      title: Plugin installati\n      plugin_link: I plugin estendono ed espandono le funzionalità. È possibile trovare plugin nel repository <1>Plugin</1>.\n      filter:\n        all: Tutto\n        active: Attivo\n        inactive: Inattivo\n        outdated: Obsoleto\n      plugins:\n        label: Plugin\n        text: Seleziona un plugin esistente.\n      name: Nome\n      version: Versione\n      status: Stato\n      action: Azione\n      deactivate: Disattivare\n      activate: Attivare\n      settings: Impostazioni\n    settings_users:\n      title: Utenti\n      avatar:\n        label: Avatar Predefinito\n        text: Per gli utenti senza un proprio avatar personalizzato.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: \"\\nURL della base API del provider Gravatar. Ignorato quando vuoto.\"\n      profile_editable:\n        title: Profilo modificabile\n      allow_update_display_name:\n        label: Consenti di cambiare il nome utente\n      allow_update_username:\n        label: Consenti di modificare il proprio nome utente\n      allow_update_avatar:\n        label: Consenti agli utenti di modificare l'immagine del profilo\n      allow_update_bio:\n        label: Consenti agli utenti di modificare la sezione \"su di me\"\n      allow_update_website:\n        label: Consenti agli utenti di modificare il proprio sito web\n      allow_update_location:\n        label: Consenti agli utenti di modificare la propria posizione\n    privilege:\n      title: Privilegi\n      level:\n        label: Livello di reputazione richiesto\n        text: Scegli la reputazione richiesta per i privilegi\n      msg:\n        should_be_number: l'input dovrebbe essere un numero\n        number_larger_1: il numero dovrebbe essere uguale o superiore a 1\n    badges:\n      action: Azione\n      active: Attivo\n      activate: Attivare\n      all: Tutto\n      awards: Ricompense\n      deactivate: Disattivare\n      filter:\n        placeholder: Filtra per nome, badge:id\n      group: Gruppo\n      inactive: Inattivo\n      name: Nome\n      show_logs: Visualizza i log\n      status: Stato\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (opzionale)\n    empty: non può essere vuoto\n    invalid: non è valido\n    btn_submit: Salva\n    not_found_props: \"Proprietà richiesta {{ key }} non trovata.\"\n    select: Seleziona\n  page_review:\n    review: Revisione\n    proposed: Proposto\n    question_edit: Edita domanda\n    answer_edit: Edita risposta\n    tag_edit: Edita tag\n    edit_summary: Edita il riepilogo\n    edit_question: Edita la domanda\n    edit_answer: Edita la risposta\n    edit_tag: Edita  tag\n    empty: Nessuna attività di revisione rimasta.\n    approve_revision_tip: Approvi questa revisione?\n    approve_flag_tip: Approvi questo contrassegno?\n    approve_post_tip: Approvi questo post?\n    approve_user_tip: Approvate questo utente?\n    suggest_edits: Modifiche suggerite\n    flag_post: Post contrassegnato\n    flag_user: Utente contrassegno\n    queued_post: \"Posta in coda\\n\"\n    queued_user: Utente in coda\n    filter_label: Digita\n    reputation: reputazione\n    flag_post_type: Post contrassegnato come {{ type }}.\n    flag_user_type: Utente contrassegnato come {{ type }}.\n    edit_post: Modifica il post\n    list_post: Lista il post\n    unlist_post: Rimuovi post\n  timeline:\n    undeleted: Non cancellato\n    deleted: eliminato\n    downvote: voto negativo\n    upvote: voto a favore\n    accept: accetta\n    cancelled: Cancellato\n    commented: commentato\n    rollback: ripristino\n    edited: modificato\n    answered: risposto\n    asked: chiesto\n    closed: chiuso\n    reopened: riaperto\n    created: creato\n    pin: Fissa in cima\n    unpin: Rimosso dalla cima\n    show: elencato\n    hide: non elencato\n    title: \"Cronologia per\"\n    tag_title: \"Timeline per\"\n    show_votes: \"Mostra voti\"\n    n_or_a: N/D\n    title_for_question: \"Timeline per\"\n    title_for_answer: \"Timeline per rispondere a {{ title }} di {{ author }}\"\n    title_for_tag: \"Timeline per tag\"\n    datetime: Data e ora\n    type: Tipo\n    by: Di\n    comment: Commento\n    no_data: \"Non abbiamo trovato nulla\"\n  users:\n    title: Utenti\n    users_with_the_most_reputation: Utenti con i punteggi di reputazione più alti questa settimana\n    users_with_the_most_vote: Utenti che hanno votato di più questa settimana\n    staffs: Lo staff della community\n    reputation: reputazione\n    votes: voti\n  prompt:\n    leave_page: Sei sicuro di voler lasciare questa pagina?\n    changes_not_save: Le modifiche potrebbero non essere salvate.\n  draft:\n    discard_confirm: Sei sicuro di voler eliminare la bozza?\n  messages:\n    post_deleted: Questo post è stato eliminato.\n    post_cancel_deleted: Questo post è stato ripristinato.\n    post_pin: Questo post è stato selezionato.\n    post_unpin: Questo post è stato sbloccato.\n    post_hide_list: Questo post è stato nascosto dall'elenco.\n    post_show_list: Questo post è stato mostrato in elenco.\n    post_reopen: Questo post è stato riaperto.\n    post_list: Questo post è stato inserito.\n    post_unlist: Questo post è stato rimosso.\n    post_pending: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato.\n    post_closed: Questo post è stato chiuso.\n    answer_deleted: Questa risposta è stata eliminata.\n    answer_cancel_deleted: Questa risposta è stata ripristinata.\n    change_user_role: Il ruolo di questo utente è stato cambiato.\n    user_inactive: Questo utente è già inattivo.\n    user_normal: Questo utente è già normale.\n    user_suspended: Questo utente è stato sospeso.\n    user_deleted: Questo utente è stato eliminato.\n    user_added: User has been added successfully.\n    badge_activated: Questo badge è stato attivato.\n    badge_inactivated: Questo badge è stato disattivato.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/ja_JP.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: 成功\n    unknown:\n      other: 不明なエラー\n    request_format_error:\n      other: リクエスト形式が無効です。\n    unauthorized_error:\n      other: 権限がありません。\n    database_error:\n      other: データサーバーエラー\n    forbidden_error:\n      other: アクセス権限がありません。\n    duplicate_request_error:\n      other: 重複しています\n  action:\n    report:\n      other: 通報\n    edit:\n      other: 編集\n    delete:\n      other: 削除\n    close:\n      other: 解決済み\n    reopen:\n      other: 再オープン\n    forbidden_error:\n      other: アクセス権限がありません。\n    pin:\n      other: ピン留めする\n    hide:\n      other: 限定公開にする\n    unpin:\n      other: ピン留め解除\n    show:\n      other: 限定公開を解除する\n    invite_someone_to_answer:\n      other: 編集\n    undelete:\n      other: 復元する\n    merge:\n      other: マージ\n  role:\n    name:\n      user:\n        other: ユーザー\n      admin:\n        other: 管理者\n      moderator:\n        other: モデレーター\n    description:\n      user:\n        other: 一般的なアクセスしか持ちません。\n      admin:\n        other: すべてにアクセスできる大いなる力を持っています。\n      moderator:\n        other: 管理者以外のすべての投稿へのアクセス権を持っています。\n  privilege:\n    level_1:\n      description:\n        other: レベル1 必要最低の評判レベルで利用可(クローズサイト・グループ・特定の人数下での利用)\n    level_2:\n      description:\n        other: レベル2 少しだけ評判レベルが必要(スタートアップコミュニティ・不特定多数の人数での利用)\n    level_3:\n      description:\n        other: レベル3 高い評判レベルが必要(成熟したコミュニティ)\n    level_custom:\n      description:\n        other: カスタムレベル\n    rank_question_add_label:\n      other: 質問する\n    rank_answer_add_label:\n      other: 回答を書く\n    rank_comment_add_label:\n      other: コメントを書く\n    rank_report_add_label:\n      other: 通報\n    rank_comment_vote_up_label:\n      other: コメントを高評価\n    rank_link_url_limit_label:\n      other: 一度に2つ以上のリンクを投稿する\n    rank_question_vote_up_label:\n      other: 質問を高評価\n    rank_answer_vote_up_label:\n      other: 回答を高評価\n    rank_question_vote_down_label:\n      other: 質問を低評価\n    rank_answer_vote_down_label:\n      other: 回答を低評価\n    rank_invite_someone_to_answer_label:\n      other: 誰かを回答に招待する\n    rank_tag_add_label:\n      other: 新しいタグを作成\n    rank_tag_edit_label:\n      other: タグの説明を編集（レビューが必要）\n    rank_question_edit_label:\n      other: 他の質問を編集（レビューが必要）\n    rank_answer_edit_label:\n      other: 他の回答を編集（レビューが必要）\n    rank_question_edit_without_review_label:\n      other: レビューなしで他の質問を編集する\n    rank_answer_edit_without_review_label:\n      other: レビューなしで他の回答を編集する\n    rank_question_audit_label:\n      other: 質問の編集をレビュー\n    rank_answer_audit_label:\n      other: 回答の編集をレビュー\n    rank_tag_audit_label:\n      other: タグの編集をレビュー\n    rank_tag_edit_without_review_label:\n      other: レビューなしでタグの説明を編集\n    rank_tag_synonym_label:\n      other: タグの同義語を管理する\n  email:\n    other: メールアドレス\n  e_mail:\n    other: メールアドレス\n  password:\n    other: パスワード\n  pass:\n    other: パスワード\n  old_pass:\n    other: 現在のパスワード\n  original_text:\n    other: 投稿\n  email_or_password_wrong_error:\n    other: メールアドレスとパスワードが一致しません。\n  error:\n    common:\n      invalid_url:\n        other: 無効なURL\n      status_invalid:\n        other: 無効なステータス\n    password:\n      space_invalid:\n        other: パスワードにスペースを含めることはできません。\n    admin:\n      cannot_update_their_password:\n        other: パスワードは変更できません。\n      cannot_edit_their_profile:\n        other: プロフィールを変更できません。\n      cannot_modify_self_status:\n        other: ステータスを変更できません。\n      email_or_password_wrong:\n        other: メールアドレスとパスワードが一致しません。\n    answer:\n      not_found:\n        other: 回答が見つかりません。\n      cannot_deleted:\n        other: 削除する権限がありません。\n      cannot_update:\n        other: 更新する権限がありません。\n      question_closed_cannot_add:\n        other: 質問はクローズされて、追加できません。\n      content_cannot_empty:\n        other: 回答を入力してください。\n    comment:\n      edit_without_permission:\n        other: コメントを編集することはできません。\n      not_found:\n        other: コメントが見つかりません。\n      cannot_edit_after_deadline:\n        other: コメント時間が長すぎて変更できません。\n      content_cannot_empty:\n        other: コメントを入力してください。\n    email:\n      duplicate:\n        other: メールアドレスは既に存在しています。\n      need_to_be_verified:\n        other: 電子メールを確認する必要があります。\n      verify_url_expired:\n        other: メール認証済みURLの有効期限が切れています。メールを再送信してください。\n      illegal_email_domain_error:\n        other: そのメールドメインからのメールは許可されていません。別のメールアドレスを使用してください。\n    lang:\n      not_found:\n        other: 言語ファイルが見つかりません。\n    object:\n      captcha_verification_failed:\n        other: Captchaが間違っています。\n      disallow_follow:\n        other: フォローが許可されていません。\n      disallow_vote:\n        other: 投票が許可されていません。\n      disallow_vote_your_self:\n        other: 自分の投稿には投票できません。\n      not_found:\n        other: オブジェクトが見つかりません。\n      verification_failed:\n        other: 認証に失敗しました。\n      email_or_password_incorrect:\n        other: メールアドレスとパスワードが一致しません。\n      old_password_verification_failed:\n        other: 古いパスワードの確認に失敗しました。\n      new_password_same_as_previous_setting:\n        other: 新しいパスワードは前のパスワードと同じです。\n      already_deleted:\n        other: この投稿は削除されました。\n    meta:\n      object_not_found:\n        other: メタオブジェクトが見つかりません\n    question:\n      already_deleted:\n        other: この投稿は削除されました。\n      under_review:\n        other: あなたの投稿はレビュー待ちです。承認されると表示されます。\n      not_found:\n        other: 質問が見つかりません。\n      cannot_deleted:\n        other: 削除する権限がありません。\n      cannot_close:\n        other: クローズする権限がありません。\n      cannot_update:\n        other: 更新する権限がありません。\n      content_cannot_empty:\n        other: 内容を入力してください。\n      content_less_than_minimum:\n        other: 入力された内容の文字数が足りません。\n    rank:\n      fail_to_meet_the_condition:\n        other: 評判ランクが条件を満たしていません\n      vote_fail_to_meet_the_condition:\n        other: フィードバックをありがとうございます。投票には少なくとも {{.Rank}} の評判が必要です。\n      no_enough_rank_to_operate:\n        other: 少なくとも {{.Rank}} の評判が必要です。\n    report:\n      handle_failed:\n        other: レポートの処理に失敗しました。\n      not_found:\n        other: レポートが見つかりません。\n    tag:\n      already_exist:\n        other: タグは既に存在します。\n      not_found:\n        other: タグが見つかりません。\n      recommend_tag_not_found:\n        other: おすすめタグは存在しません。\n      recommend_tag_enter:\n        other: 少なくとも 1 つの必須タグを入力してください。\n      not_contain_synonym_tags:\n        other: 同義語のタグを含めないでください。\n      cannot_update:\n        other: 更新する権限がありません。\n      is_used_cannot_delete:\n        other: 使用中のタグは削除できません。\n      cannot_set_synonym_as_itself:\n        other: 現在のタグの同義語をそのものとして設定することはできません。\n      minimum_count:\n        other: タグが不足しています。\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Fromの名前はメールアドレスにできません。\n    theme:\n      not_found:\n        other: テーマが見つかりません。\n    revision:\n      review_underway:\n        other: 現在編集できません。レビューキューにバージョンがあります。\n      no_permission:\n        other: 編集する権限がありません。\n    user:\n      external_login_missing_user_id:\n        other: サードパーティのプラットフォームは一意のユーザーIDを提供していないため、ログインできません。ウェブサイト管理者にお問い合わせください。\n      external_login_unbinding_forbidden:\n        other: ログインを削除する前に、アカウントのログインパスワードを設定してください。\n      email_or_password_wrong:\n        other:\n          other: メールアドレスとパスワードが一致しません。\n      not_found:\n        other: ユーザーが見つかりません。\n      suspended:\n        other: このユーザーは凍結されています\n      username_invalid:\n        other: 無効なユーザー名です！\n      username_duplicate:\n        other: ユーザー名は既に使用されています！\n      set_avatar:\n        other: アバターを設定できませんでした\n      cannot_update_your_role:\n        other: ロールを変更できません\n      not_allowed_registration:\n        other: 現在、このサイトは新規登録を受け付けておりません\n      not_allowed_login_via_password:\n        other: 現在、このサイトはパスワードでログインできません\n      access_denied:\n        other: アクセスが拒否されました\n      page_access_denied:\n        other: このページへのアクセス権がありません\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"一度に追加するユーザーの数は、1 -{{.MaxAmount}} の範囲にする必要があります。\"\n      status_suspended_forever:\n        other: \"<strong>このユーザーは永久に停止されました。</strong> このユーザーはコミュニティガイドラインに準拠していません。\"\n      status_suspended_until:\n        other: \"<strong>このユーザーは {{.SuspendedUntil}} まで利用停止となりました。</strong> このユーザーはコミュニティ ガイドラインに準拠していません。\"\n      status_deleted:\n        other: \"このユーザーは削除されました。\"\n      status_inactive:\n        other: \"このユーザーは非アクティブです。\"\n    config:\n      read_config_failed:\n        other: configの読み込みに失敗しました\n    database:\n      connection_failed:\n        other: データベースの接続が失敗しました\n      create_table_failed:\n        other: テーブルの作成に失敗しました\n    install:\n      create_config_failed:\n        other: config.yaml を作成できません。\n    upload:\n      unsupported_file_format:\n        other: サポートされていないファイル形式です。\n    site_info:\n      config_not_found:\n        other: configが見つかりません。\n    badge:\n      object_not_found:\n        other: バッジオブジェクトが見つかりません\n  reason:\n    spam:\n      name:\n        other: スパム\n      desc:\n        other: この投稿は広告です。現在のトピックには有用ではありません。\n    rude_or_abusive:\n      name:\n        other: 誹謗中傷\n      desc:\n        other: \"合理的な人は、このコンテンツを尊重する言説には不適切と判断するでしょう。\"\n    a_duplicate:\n      name:\n        other: 重複\n      desc:\n        other: この質問は以前に質問されており、すでに回答があります。\n      placeholder:\n        other: 既存の質問リンクを入力してください\n    not_a_answer:\n      name:\n        other: 回答では無い\n      desc:\n        other: \"これは答えとして投稿されましたが、質問に答えようとしません。 それはおそらく編集、コメント、別の質問、または完全に削除されるべきです。\"\n    no_longer_needed:\n      name:\n        other: 必要では無い\n      desc:\n        other: このコメントは古く、この投稿とは関係がありません。\n    something:\n      name:\n        other: その他\n      desc:\n        other: 上記以外の理由でスタッフの注意が必要です。\n      placeholder:\n        other: あなたが懸念していることを私たちに教えてください\n    community_specific:\n      name:\n        other: コミュニティ固有の理由です\n      desc:\n        other: この質問はコミュニティガイドラインを満たしていません\n    not_clarity:\n      name:\n        other: 詳細や明快さが必要です\n      desc:\n        other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。\n    looks_ok:\n      name:\n        other: LGTM\n      desc:\n        other: この投稿はそのままで良く、改善する必要はありません！\n    needs_edit:\n      name:\n        other: 編集する必要があったため変更しました。\n      desc:\n        other: 自分自身でこの投稿の問題を改善し修正します。\n    needs_close:\n      name:\n        other: クローズする必要がある\n      desc:\n        other: クローズされた質問は回答できませんが、編集、投票、コメントはできます。\n    needs_delete:\n      name:\n        other: 削除が必要です\n      desc:\n        other: この投稿は削除されました\n  question:\n    close:\n      duplicate:\n        name:\n          other: スパム\n        desc:\n          other: この質問は以前に質問されており、すでに回答があります。\n      guideline:\n        name:\n          other: コミュニティ固有の理由です\n        desc:\n          other: この質問はコミュニティガイドラインを満たしていません\n      multiple:\n        name:\n          other: 詳細や明快さが必要です\n        desc:\n          other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。\n      other:\n        name:\n          other: その他\n        desc:\n          other: 上記以外の理由でスタッフの注意が必要です。\n    operation_type:\n      asked:\n        other: 質問済み\n      answered:\n        other: 回答済み\n      modified:\n        other: 修正済み\n    deleted_title:\n      other: 質問を削除\n    questions_title:\n      other: 質問\n  tag:\n    tags_title:\n      other: タグ\n    no_description:\n      other: タグには説明がありません。\n  notification:\n    action:\n      update_question:\n        other: 質問を更新\n      answer_the_question:\n        other: 回答済みの質問\n      update_answer:\n        other: 回答を更新\n      accept_answer:\n        other: 承認された回答\n      comment_question:\n        other: コメントされた質問\n      comment_answer:\n        other: コメントされた回答\n      reply_to_you:\n        other: あなたへの返信\n      mention_you:\n        other: メンションされました\n      your_question_is_closed:\n        other: あなたの質問はクローズされました\n      your_question_was_deleted:\n        other: あなたの質問は削除されました\n      your_answer_was_deleted:\n        other: あなたの質問は削除されました\n      your_comment_was_deleted:\n        other: あなたのコメントは削除されました\n      up_voted_question:\n        other: 質問を高評価\n      down_voted_question:\n        other: 質問を低評価\n      up_voted_answer:\n        other: 回答を高評価\n      down_voted_answer:\n        other: 回答を低評価\n      up_voted_comment:\n        other: コメントを高評価\n      invited_you_to_answer:\n        other: あなたを回答に招待しました\n      earned_badge:\n        other: '\"{{.BadgeName}}\"バッジを獲得しました'\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] 新しいメールアドレスを確認してください\"\n      body:\n        other: \"{{.SiteName}}の新しいメールアドレスを確認しリンクをクリックしてください。<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\n身に覚えがない場合はこのメールを無視してください。\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} があなたの質問に回答しました\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>{{.SiteName}}で確認</a><br><br>\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} があなたを回答に招待しました\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>あなたなら答えを知っているかもしれません。</blockquote><br>\\n<a href='{{.InviteUrl}}'>{{.SiteName}}で確認</a><br><br>\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>配信停止</a></small>\\n\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} があなたの投稿にコメントしました\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>で確認{{.SiteName}}</a><br><br>\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>配信停止</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] 新しい質問： {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\n注: これはシステムから送信される自動メールです。ご返信いただいても返信は表示されませんので、ご返信はご遠慮ください。<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>購読解除</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] パスワードリセット\"\n      body:\n        other: \"{{.SiteName}}であなたのパスワードをリセットしようとしました。<br><br>\\n\\nもしお心当たりがない場合は、このメールを無視してください。<br><br>\\n\\n新しいパスワードを設定するには、以下のリンクをクリックしてください。<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\\n\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] 新しいアカウントを確認\"\n      body:\n        other: \"{{.SiteName}}へようこそ！<br><br>\\n\\n以下のリンクをクリックして、新しいアカウントを確認・有効化してください。<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\n上記のリンクがクリックできない場合は、リンクをコピーしてブラウザのアドレスバーに貼り付けてみてください。\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] テストメール\"\n      body:\n        other: \"これはテストメールです。\\n<br><br>\\n\\n--<br>\\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。<br><br>\"\n  action_activity_type:\n    upvote:\n      other: 高評価\n    upvoted:\n      other: 高評価しました\n    downvote:\n      other: 低評価\n    downvoted:\n      other: 低評価しました\n    accept:\n      other: 承認\n    accepted:\n      other: 承認済み\n    edit:\n      other: 編集\n  review:\n    queued_post:\n      other: キューに入れられた投稿\n    flagged_post:\n      other: 投稿を通報\n    suggested_post_edit:\n      other: 提案された編集\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} と {{ .Count }} もっと...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: 自伝作家\n        desc:\n          other: <a href=\"{{ .ProfileURL }}\" target=\"_blank\">プロファイル</a> 情報を入力しました。\n      certified:\n        name:\n          other: 認定済み\n        desc:\n          other: 新しいユーザーがチュートリアルを完了しました。\n      editor:\n        name:\n          other: 編集者\n        desc:\n          other: 最初の投稿の編集\n      first_flag:\n        name:\n          other: 初めての報告\n        desc:\n          other: 初めての報告\n      first_upvote:\n        name:\n          other: はじめての高評価\n        desc:\n          other: はじめて投稿に高評価した\n      first_link:\n        name:\n          other: はじめてのリンク\n        desc:\n          other: 初めて別の投稿へのリンクを追加した。\n      first_reaction:\n        name:\n          other: 初めてのリアクション\n        desc:\n          other: はじめて投稿にリアクションした\n      first_share:\n        name:\n          other: はじめての共有\n        desc:\n          other: はじめて投稿を共有した\n      scholar:\n        name:\n          other: 研究生\n        desc:\n          other: 質問をして回答が承認された\n      commentator:\n        name:\n          other: コメントマン\n        desc:\n          other: 5つのコメントをした\n      new_user_of_the_month:\n        name:\n          other: 今月の新しいユーザー\n        desc:\n          other: 最初の1ヶ月で優れた貢献をした\n      read_guidelines:\n        name:\n          other: ガイドラインを読んだ\n        desc:\n          other: '「コミュニティガイドライン」を読んだ'\n      reader:\n        name:\n          other: リーダー\n        desc:\n          other: 10以上の回答を持つトピックのすべての回答を読んだ\n      welcome:\n        name:\n          other: ようこそ！\n        desc:\n          other: 高評価をされた\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: 25人の訪問者と投稿を共有した\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: 300の訪問者と投稿を共有した\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: 1000人の訪問者と投稿を共有した\n      out_of_love:\n        name:\n          other: お隣さん\n        desc:\n          other: 1日に50票いれた\n      higher_love:\n        name:\n          other: お友達\n        desc:\n          other: 5日目に50回投票した\n      crazy_in_love:\n        name:\n          other: 崇拝\n        desc:\n          other: 一日に50回投票を20回した\n      promoter:\n        name:\n          other: プロモーター\n        desc:\n          other: ユーザーを招待した\n      campaigner:\n        name:\n          other: キャンペーン\n        desc:\n          other: 3人のベーシックユーザーを招待しました。\n      champion:\n        name:\n          other: チャンピオン\n        desc:\n          other: 5人のメンバーを招待しました。\n      thank_you:\n        name:\n          other: Thank you\n        desc:\n          other: 投稿が20件！投票数が10件！\n      gives_back:\n        name:\n          other: 返品\n        desc:\n          other: 投稿が100件 !?!? 投票数が100件 !?!?\n      empathetic:\n        name:\n          other: 共感性\n        desc:\n          other: 500の投稿を投票し、1000の投票を与えた。\n      enthusiast:\n        name:\n          other: 楽天家\n        desc:\n          other: 10日間連続ログイン\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: 100日連続ログイン！！！\n      devotee:\n        name:\n          other: 献身者\n        desc:\n          other: 365日連続訪問！！！！！！！！\n      anniversary:\n        name:\n          other: 周年記念\n        desc:\n          other: 年に一回は...\n      appreciated:\n        name:\n          other: ありがとう！\n        desc:\n          other: 20件の投稿に1件の投票を受け取った\n      respected:\n        name:\n          other: 尊敬される\n        desc:\n          other: 100件の投稿で2件の投票を受け取った\n      admired:\n        name:\n          other: 崇拝された\n        desc:\n          other: 300の投稿に5票を獲得した\n      solved:\n        name:\n          other: 解決\n        desc:\n          other: 答えを受け入れられた\n      guidance_counsellor:\n        name:\n          other: アドバイザー\n        desc:\n          other: 10個の回答が承認された\n      know_it_all:\n        name:\n          other: 物知り博士\n        desc:\n          other: 50個の回答が承認された\n      solution_institution:\n        name:\n          other: 解決機関\n        desc:\n          other: 150個の回答が承認された\n      nice_answer:\n        name:\n          other: 素敵な回答\n        desc:\n          other: 回答スコアは10以上！！\n      good_answer:\n        name:\n          other: 良い回答\n        desc:\n          other: 回答スコアは25以上！？！\n      great_answer:\n        name:\n          other: 素晴らしい回答\n        desc:\n          other: 回答スコアは50以上！！！！\n      nice_question:\n        name:\n          other: ナイスな質問\n        desc:\n          other: 質問スコアは10以上！！\n      good_question:\n        name:\n          other: よい質問\n        desc:\n          other: 質問スコアは25以上！？！\n      great_question:\n        name:\n          other: 素晴らしい質問\n        desc:\n          other: 50人の閲覧者！！\n      popular_question:\n        name:\n          other: 人気のある質問\n        desc:\n          other: 500人の閲覧者！！！\n      notable_question:\n        name:\n          other: 注目すべき質問\n        desc:\n          other: 1,000人の閲覧者！！！！\n      famous_question:\n        name:\n          other: 偉大な質問\n        desc:\n          other: 5,000人の閲覧者！！！！！\n      popular_link:\n        name:\n          other: 人気のリンク\n        desc:\n          other: 外部リンクを50回クリック\n      hot_link:\n        name:\n          other: 激アツリンク\n        desc:\n          other: 外部リンクを300回クリック\n      famous_link:\n        name:\n          other: 有名なリンク\n        desc:\n          other: 外部リンクを100回クリック\n    default_badge_groups:\n      getting_started:\n        name:\n          other: はじめに\n      community:\n        name:\n          other: コミュニティ\n      posting:\n        name:\n          other: 投稿中\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: 書式設定\n    desc: \\n\n  pagination:\n    prev: 前へ\n    next: 次へ\n  page_title:\n    question: 質問\n    questions: 質問\n    tag: タグ\n    tags: タグ\n    tag_wiki: タグ wiki\n    create_tag: タグを作成\n    edit_tag: タグを編集\n    ask_a_question: 質問を作成\n    edit_question: 質問を編集\n    edit_answer: 回答を編集\n    search: 検索\n    posts_containing: 記事を含む投稿\n    settings: 設定\n    notifications: お知らせ\n    login: ログイン\n    sign_up: 新規登録\n    account_recovery: アカウントの復旧\n    account_activation: アカウント有効化\n    confirm_email: メールアドレスを確認\n    account_suspended: アカウントは凍結されています\n    admin: 管理者\n    change_email: メールアドレスを変更\n    install: 回答に応答する\n    upgrade: 回答を改善する\n    maintenance: ウェブサイトのメンテナンス\n    users: ユーザー\n    oauth_callback: 処理中\n    http_404: HTTP エラー 404\n    http_50X: HTTP エラー 500\n    http_403: HTTP エラー 403\n    logout: ログアウト\n    posts: 投稿\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: 通知\n    inbox: 受信トレイ\n    achievement: 実績\n    new_alerts: 新しい通知\n    all_read: すべて既読にする\n    show_more: もっと見る\n    someone: 誰か\n    inbox_type:\n      all: すべて\n      posts: 投稿\n      invites: 招待\n      votes: 投票\n    answer: 回答\n    question: 質問\n    badge_award: バッジ\n  suspended:\n    title: あなたのアカウントは停止されています。\n    until_time: \"あなたのアカウントは {{ time }} まで停止されました。\"\n    forever: このユーザーは永久に停止されました。\n    end: コミュニティガイドラインを満たしていません。\n    contact_us: お問い合わせ\n  editor:\n    blockquote:\n      text: 引用\n    bold:\n      text: 強い\n    chart:\n      text: チャート\n      flow_chart: フローチャート\n      sequence_diagram: シーケンス図\n      class_diagram: クラス図\n      state_diagram: 状態図\n      entity_relationship_diagram: ER図\n      user_defined_diagram: ユーザー定義図\n      gantt_chart: ガントチャート\n      pie_chart: 円グラフ\n    code:\n      text: コードサンプル\n      add_code: コードサンプルを追加\n      form:\n        fields:\n          code:\n            label: コード\n            msg:\n              empty: Code を空にすることはできません。\n          language:\n            label: 言語\n            placeholder: 自動検出\n      btn_cancel: キャンセル\n      btn_confirm: 追加\n    formula:\n      text: 数式\n      options:\n        inline: インライン数式\n        block: ブロック数式\n    heading:\n      text: 見出し\n      options:\n        h1: 見出し1\n        h2: 見出し2\n        h3: 見出し3\n        h4: 見出し4\n        h5: 見出し5\n        h6: 見出し6\n    help:\n      text: ヘルプ\n    hr:\n      text: 水平方向の罫線\n    image:\n      text: 画像\n      add_image: 画像を追加する\n      tab_image: 画像をアップロードする\n      form_image:\n        fields:\n          file:\n            label: 画像ファイル\n            btn: 画像を選択する\n            msg:\n              empty: ファイルは空にできません。\n              only_image: 画像ファイルのみが許可されています。\n              max_size: ファイルサイズは {{size}} MBを超えることはできません。\n          desc:\n            label: 説明\n      tab_url: 画像URL\n      form_url:\n        fields:\n          url:\n            label: 画像URL\n            msg:\n              empty: 画像のURLは空にできません。\n          name:\n            label: 説明\n      btn_cancel: キャンセル\n      btn_confirm: 追加\n      uploading: アップロード中\n    indent:\n      text: インデント\n    outdent:\n      text: アウトデント\n    italic:\n      text: 斜体\n    link:\n      text: ハイパーリンク\n      add_link: ハイパーリンクを追加\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URLを入力してください。\n          name:\n            label: 説明\n      btn_cancel: キャンセル\n      btn_confirm: 追加\n    ordered_list:\n      text: 順序付きリスト\n    unordered_list:\n      text: 箇条書きリスト\n    table:\n      text: ' テーブル'\n      heading: 見出し\n      cell: セル\n    file:\n      text: ファイルを添付\n      not_supported: \"そのファイルタイプをサポートしていません。 {{file_type}} でもう一度お試しください。\"\n      max_size: \"添付ファイルサイズは {{size}} MB を超えることはできません。\"\n  close_modal:\n    title: この投稿を次のように閉じます...\n    btn_cancel: キャンセル\n    btn_submit: 送信\n    remark:\n      empty: 入力してください。\n    msg:\n      empty: 理由を選んでください。\n  report_modal:\n    flag_title: この投稿を報告するフラグを立てています...\n    close_title: この投稿を次のように閉じます...\n    review_question_title: 質問の編集をレビュー\n    review_answer_title: 答えをレビューする\n    review_comment_title: レビューコメント\n    btn_cancel: キャンセル\n    btn_submit: 送信\n    remark:\n      empty: 入力してください。\n    msg:\n      empty: 理由を選んでください。\n      not_a_url: URL形式が正しくありません。\n      url_not_match: URL の原点が現在のウェブサイトと一致しません。\n  tag_modal:\n    title: 新しいタグを作成\n    form:\n      fields:\n        display_name:\n          label: 表示名\n          msg:\n            empty: 表示名を入力してください。\n            range: 表示名は最大 35 文字までです。\n        slug_name:\n          label: URLスラッグ\n          desc: '文字セット「a-z」、「0-9」、「+ # -」を使用する必要があります。'\n          msg:\n            empty: URL スラッグを空にすることはできません。\n            range: タイトルは最大35文字までです.\n            character: URL スラグに許可されていない文字セットが含まれています。\n        desc:\n          label: 説明\n        revision:\n          label: 修正\n        edit_summary:\n          label: 概要を編集\n          placeholder: >-\n            簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット)\n    btn_cancel: キャンセル\n    btn_submit: 送信\n    btn_post: 新しいタグを投稿\n  tag_info:\n    created_at: 作成\n    edited_at: 編集済\n    history: 履歴\n    synonyms:\n      title: 類義語\n      text: 次のタグが再マップされます\n      empty: 同義語は見つかりません。\n      btn_add: 同義語を追加\n      btn_edit: 編集\n      btn_save: 保存\n    synonyms_text: 次のタグが再マップされます\n    delete:\n      title: このタグを削除\n      tip_with_posts: >-\n        <p> <strong>同義語</strong>でタグを削除することはできません。</p> <p>最初にこのタグから同義語を削除してください。</p>\n      tip_with_synonyms: >-\n        <p> <strong>同義語</strong>でタグを削除することはできません。</p> <p>最初にこのタグから同義語を削除してください。</p>\n      tip: 本当に削除してもよろしいですか？\n      close: クローズ\n    merge:\n      title: タグをマージ\n      source_tag_title: ソース タグ\n      source_tag_description: ソースタグとそれに関連付けられたデータは、ターゲットタグに再マップされます。\n      target_tag_title: ターゲットタグ\n      target_tag_description: マージ後、これら2つのタグの同義語が作成されます。\n      no_results: 一致するタグはありません\n      btn_submit: 送信\n      btn_close: 閉じる\n  edit_tag:\n    title: タグを編集\n    default_reason: タグを編集\n    default_first_reason: タグを追加\n    btn_save_edits: 編集を保存\n    btn_cancel: キャンセル\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"YYYY年MM月D日\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: 今\n    x_seconds_ago: \"{{count}}秒前\"\n    x_minutes_ago: \"{{count}}分前\"\n    x_hours_ago: \"{{count}}時間前\"\n    hour: 時\n    day: 日\n    hours: 時\n    days: 日\n    month: 月\n    months: ヶ月\n    year: 年\n  reaction:\n    heart: ハート\n    smile: 笑顔\n    frown: 眉をひそめる\n    btn_label: リアクションの追加または削除\n    undo_emoji: '{{ emoji }} のリアクションを元に戻す'\n    react_emoji: '{{ emoji }} に反応する'\n    unreact_emoji: '{{ emoji }} に反応しない'\n  comment:\n    btn_add_comment: コメントを追加\n    reply_to: 返信：\n    btn_reply: 返信\n    btn_edit: 編集\n    btn_delete: 削除\n    btn_flag: フラグ\n    btn_save_edits: 編集内容を保存\n    btn_cancel: キャンセル\n    show_more: \"{{count}} 件のその他のコメント\"\n    tip_question: >-\n      コメントを使用して、より多くの情報を求めたり、改善を提案したりします。コメントで質問に答えることは避けてください。\n    tip_answer: >-\n      コメントを使用して他のユーザーに返信したり、変更を通知します。新しい情報を追加する場合は、コメントの代わりに投稿を編集します。\n    tip_vote: 投稿に役に立つものを追加します\n  edit_answer:\n    title: 回答を編集\n    default_reason: 回答を編集\n    default_first_reason: 回答を追加\n    form:\n      fields:\n        revision:\n          label: 修正\n        answer:\n          label: 回答\n          feedback:\n            characters: コンテンツは6文字以上でなければなりません。\n        edit_summary:\n          label: 概要を編集\n          placeholder: >-\n            簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット)\n    btn_save_edits: 編集を保存\n    btn_cancel: キャンセル\n  tags:\n    title: タグ\n    sort_buttons:\n      popular: 人気\n      name: 名前\n      newest: 最新\n    button_follow: フォロー\n    button_following: フォロー中\n    tag_label: 質問\n    search_placeholder: タグ名でフィルタ\n    no_desc: タグには説明がありません。\n    more: もっと見る\n    wiki: Wiki\n  ask:\n    title: 質問を作成\n    edit_title: 質問を編集\n    default_reason: 質問を編集\n    default_first_reason: 質問を作成\n    similar_questions: 類似の質問\n    form:\n      fields:\n        revision:\n          label: 修正\n        title:\n          label: タイトル\n          placeholder: どのようなトピックですか？具体的に教えてください。\n          msg:\n            empty: タイトルを空にすることはできません。\n            range: タイトルは最大150文字までです\n        body:\n          label: 本文\n          msg:\n            empty: 本文を空にすることはできません。\n          hint:\n            optional_body: 質問を記載してください。\n            minimum_characters: \"質問を記載してください。{{min_content_length}} 文字以上の記載が必要です。\"\n        tags:\n          label: タグ\n          msg:\n            empty: 少なくとも一つ以上のタグが必要です。\n        answer:\n          label: 回答\n          msg:\n            empty: 回答は空欄にできません\n        edit_summary:\n          label: 概要を編集\n          placeholder: >-\n            簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット)\n    btn_post_question: 質問を投稿する\n    btn_save_edits: 編集内容を保存\n    answer_question: ご自身の質問に答えてください\n    post_question&answer: 質問と回答を投稿する\n  tag_selector:\n    add_btn: タグを追加\n    create_btn: 新しタグを作成\n    search_tag: タグを検索\n    hint: 内容を記載してください。1つ以上のタグが必要です。\n    hint_zero_tags: 内容を記載してください。\n    hint_more_than_one_tag: \"内容を記載してください。{{min_content_length}} 文字以上の記載が必要です。\"\n    no_result: 一致するタグはありません\n    tag_required_text: 必須タグ (少なくとも 1 つ)\n  header:\n    nav:\n      question: 質問\n      tag: タグ\n      user: ユーザー\n      badges: バッジ\n      profile: プロフィール\n      setting: 設定\n      logout: ログアウト\n      admin: 管理者\n      review: レビュー\n      bookmark: ブックマーク\n      moderation: モデレーション\n    search:\n      placeholder: 検索\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: 変更\n    loading: 読み込み中…\n  pic_auth_code:\n    title: CAPTCHA\n    placeholder: 上記のテキストを入力してください\n    msg:\n      empty: Code を空にすることはできません。\n  inactive:\n    first: >-\n      <bold>{{mail}}</bold>にアクティベーションメールを送信しました。メールの指示に従ってアカウントをアクティベーションしてください。\n    info: \"届かない場合は、迷惑メールフォルダを確認してください。\"\n    another: >-\n      <bold>{{mail}}</bold>に別のアクティベーションメールを送信しました。 到着には数分かかる場合があります。スパムフォルダを確認してください。\n    btn_name: 認証メールを再送信\n    change_btn_name: メールアドレスを変更\n    msg:\n      empty: 空にすることはできません\n    resend_email:\n      url_label: 本当に認証メールを再送信してもよろしいですか？\n      url_text: ユーザーに上記のアクティベーションリンクを渡すこともできます。\n  login:\n    login_to_continue: ログインして続行\n    info_sign: アカウントをお持ちではありませんか？<1>サインアップ</1>\n    info_login: すでにアカウントをお持ちですか？<1>ログイン</1>\n    agreements: 登録することにより、<1>プライバシーポリシー</1>および<3>サービス規約</3>に同意するものとします。\n    forgot_pass: パスワードをお忘れですか?\n    name:\n      label: 名前\n      msg:\n        empty: 名前を空にすることはできません。\n        range: 2～30文字の名前を設定してください。\n        character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです'\n    email:\n      label: メールアドレス\n      msg:\n        empty: メールアドレスを入力してください。\n    password:\n      label: パスワード\n      msg:\n        empty: パスワードを入力してください。\n        different: 入力されたパスワードが一致しません\n  account_forgot:\n    page_title: パスワードを忘れた方はこちら\n    btn_name: 回復用のメールを送る\n    send_success: >-\n      アカウントが <strong>{{mail}}</strong>と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。\n    email:\n      label: メールアドレス\n      msg:\n        empty: メールアドレスを入力してください。\n  change_email:\n    btn_cancel: キャンセル\n    btn_update: メールアドレスを更新\n    send_success: >-\n      アカウントが <strong>{{mail}}</strong>と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。\n    email:\n      label: 新しいメールアドレス\n      msg:\n        empty: メールアドレス欄を空白にしておくことはできません\n  oauth:\n    connect: '{{ auth_name }} と接続'\n    remove: '{{ auth_name }} を削除'\n  oauth_bind_email:\n    subtitle: アカウントに回復用のメールアドレスを追加します。\n    btn_update: メールアドレスを更新\n    email:\n      label: メールアドレス\n      msg:\n        empty: メールアドレスは空にできません。\n    modal_title: このメールアドレスはすでに存在しています。\n    modal_content: このメールアドレスは既に登録されています。既存のアカウントに接続してもよろしいですか？\n    modal_cancel: メールアドレスを変更する\n    modal_confirm: 既存のアカウントに接続\n  password_reset:\n    page_title: パスワード再設定\n    btn_name: パスワードをリセット\n    reset_success: >-\n      パスワードを変更しました。ログインページにリダイレクトされます。\n    link_invalid: >-\n      申し訳ありませんが、このパスワードリセットのリンクは無効になりました。パスワードが既にリセットされている可能性がありますか？\n    to_login: ログインページへ\n    password:\n      label: パスワード\n      msg:\n        empty: パスワードを入力してください。\n        length: 長さは 8 から 32 の間である必要があります\n        different: 両側に入力されたパスワードが一致しません\n    password_confirm:\n      label: 新しいパスワードの確認\n  settings:\n    page_title: 設定\n    goto_modify: 変更を開く\n    nav:\n      profile: プロフィール\n      notification: お知らせ\n      account: アカウント\n      interface: 外観\n    profile:\n      heading: プロフィール\n      btn_name: 保存\n      display_name:\n        label: 表示名\n        msg: 表示名は必須です。\n        msg_range: 表示名は 2 ~ 30 文字で入力してください。\n      username:\n        label: ユーザー名\n        caption: ユーザーは \"@username\" としてあなたをメンションできます。\n        msg: ユーザー名は空にできません。\n        msg_range: ユーザー名は2 ~ 30文字で入力してください。\n        character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです'\n      avatar:\n        label: プロフィール画像\n        gravatar: Gravatar\n        gravatar_text: 画像を変更できます：\n        custom: カスタム\n        custom_text: 画像をアップロードできます。\n        default: システム\n        msg: アバターをアップロードしてください\n      bio:\n        label: 自己紹介\n      website:\n        label: ウェブサイト\n        placeholder: \"http://example.com\"\n        msg: ウェブサイトの形式が正しくありません\n      location:\n        label: ロケーション\n        placeholder: \"都道府県,国\"\n    notification:\n      heading: メール通知\n      turn_on: ONにする\n      inbox:\n        label: 受信トレイの通知\n        description: 質問、コメント、招待状などへの回答。\n      all_new_question:\n        label: すべての新規質問\n        description: すべての新しい質問の通知を受け取ります。週に最大50問まで。\n      all_new_question_for_following_tags:\n        label: 以下のタグに対するすべての新しい質問\n        description: タグをフォローするための新しい質問の通知を受け取る。\n    account:\n      heading: アカウント\n      change_email_btn: メールアドレスを変更する\n      change_pass_btn: パスワードを変更する\n      change_email_info: >-\n        このアドレスにメールを送信しました。メールの指示に従って確認処理を行ってください。\n      email:\n        label: メールアドレス\n      new_email:\n        label: 新しいメールアドレス\n        msg: 新しいメールアドレスは空にできません。\n      pass:\n        label: 現在のパスワード\n        msg: パスワードは空白にできません\n      password_title: パスワード\n      current_pass:\n        label: 現在のパスワード\n        msg:\n          empty: 現在のパスワードが空欄です\n          length: 長さは 8 から 32 の間である必要があります.\n          different: パスワードが一致しません。\n      new_pass:\n        label: 新しいパスワード\n      pass_confirm:\n        label: 新しいパスワードの確認 \n    interface:\n      heading: 外観\n      lang:\n        label: インタフェース言語\n        text: ユーザーインターフェイスの言語。ページを更新すると変更されます。\n    my_logins:\n      title: ログイン情報\n      label: これらのアカウントを使用してログインまたはこのサイトでサインアップします。\n      modal_title: ログイン情報を削除\n      modal_content: このログイン情報をアカウントから削除してもよろしいですか？\n      modal_confirm_btn: 削除\n      remove_success: 削除に成功しました\n  toast:\n    update: 更新に成功しました\n    update_password: パスワードの変更に成功しました。\n    flag_success: フラグを付けてくれてありがとう\n    forbidden_operate_self: 自分自身で操作することはできません\n    review: レビュー後にあなたのリビジョンが表示されます。\n    sent_success: 正常に送信されました。\n  related_question:\n    title: 関連\n    answers: 回答\n  linked_question:\n    title: リンク済\n    description: リンクされた投稿\n    no_linked_question: このコンテンツからリンクされたコンテンツはありません。\n  invite_to_answer:\n    title: 回答をリクエスト\n    desc: 答えられそうな人を招待します。\n    invite: 回答に招待する\n    add: ユーザーを追加\n    search: ユーザーを検索\n  question_detail:\n    action: 動作\n    created: 作成済\n    Asked: 質問済み\n    asked: 質問済み\n    update: 修正済み\n    Edited: 編集済\n    edit: 編集済み\n    commented: コメントしました\n    Views: 閲覧回数\n    Follow: フォロー\n    Following: フォロー中\n    follow_tip: この質問をフォローして通知を受け取る\n    answered: 回答済み\n    closed_in: クローズまで\n    show_exist: 既存の質問を表示します\n    useful: 役に立った\n    question_useful: それは有用で明確です。\n    question_un_useful: 不明確または有用ではない\n    question_bookmark: この質問をブックマークする\n    answer_useful: 役に立った\n    answer_un_useful: 役に立たない\n    answers:\n      title: 回答\n      score: スコア\n      newest: 最新\n      oldest: 古い順\n      btn_accept: 承認\n      btn_accepted: 承認済み\n    write_answer:\n      title: あなたの回答\n      edit_answer: 既存の回答を編集する\n      btn_name: 回答を投稿する\n      add_another_answer: 別の回答を追加\n      confirm_title: 回答を続ける\n      continue: 続行\n      confirm_info: >-\n         <p>本当に別の答えを追加したいのですか？ </p><p>代わりに、編集リンクを使って既存の答えを洗練させ、改善することができます。</p>\n      empty: 回答は空欄にできません\n      characters: コンテンツは6文字以上でなければなりません。\n      tips:\n        header_1: ご回答ありがとうございます。\n        li1_1: \" 必ず<strong>質問に答えてください</strong>。詳細を述べ、あなたの研究を共有してください。\\n\"\n        li1_2: 参考文献や個人的な経験による裏付けを取ること。.\n        header_2: しかし、 <strong>は</strong> を避けてください...\n        li2_1: 助けを求める、説明を求める、または他の答えに応答する。\n    reopen:\n      confirm_btn: 再オープン\n      title: この投稿を再度開く\n      content: 再オープンしてもよろしいですか？\n    list:\n      confirm_btn: 一覧\n      title: この投稿の一覧\n      content: 一覧表示してもよろしいですか？\n    unlist:\n      confirm_btn: 限定公開にする\n      title: この投稿を元に戻す\n      content: 本当に元に戻しますか？\n    pin:\n      title: この投稿をピン留めする\n      content: \"グローバルに固定してもよろしいですか？\\nこの投稿はすべての投稿リストの上部に表示されます。\"\n      confirm_btn: ピン留めする\n  delete:\n    title: この投稿を削除\n    question: >-\n      <p><strong>承認された回答を削除することはお勧めしません</strong>。削除すると、今後の読者がこの知識を得られなくなってしまうからです。</p> 承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか？\n\n    answer_accepted: >-\n      <p><strong>承認された回答を削除することはお勧めしません</strong>。削除すると、今後の読者がこの知識を得られなくなってしまうからです。</p> 承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか？\n\n    other: 本当に削除してもよろしいですか？\n    tip_answer_deleted: この回答は削除されました\n    undelete_title: この投稿を元に戻す\n    undelete_desc: 本当に元に戻しますか？\n  btns:\n    confirm: 確認\n    cancel: キャンセル\n    edit: 編集\n    save: 保存\n    delete: 削除\n    undelete: 元に戻す\n    list: 限定公開を解除する\n    unlist: 限定公開にする\n    unlisted: 限定公開済み\n    login: ログイン\n    signup: 新規登録\n    logout: ログアウト\n    verify: 認証\n    create: 作成\n    approve: 承認\n    reject: 却下\n    skip: スキップする\n    discard_draft: 下書きを破棄\n    pinned: ピン留めしました\n    all: すべて\n    question: 質問\n    answer: 回答\n    comment: コメント\n    refresh: 更新\n    resend: 再送\n    deactivate: 無効化する\n    active: 有効\n    suspend: 凍結\n    unsuspend: 凍結解除\n    close: クローズ\n    reopen: 再オープン\n    ok: OK\n    light: ライト\n    dark: ダーク\n    system_setting: システム設定\n    default: 既定\n    reset: リセット\n    tag: タグ\n    post_lowercase: 投稿\n    filter: フィルター\n    ignore: 除外\n    submit: 送信\n    normal: 通常\n    closed: クローズ済み\n    deleted: 削除済み\n    deleted_permanently: 完全に削除する\n    pending: 処理待ち\n    more: もっと見る\n    view: 表示方法\n    card: カード\n    compact: コンパクト\n    display_below: 以下に表示\n    always_display: 常に表示\n    or: または\n    back_sites: サイトに戻る\n  search:\n    title: 検索結果\n    keywords: キーワード\n    options: オプション\n    follow: フォロー\n    following: フォロー中\n    counts: \"結果：{{count}}\"\n    counts_loading: \"... 結果\"\n    more: もっと見る\n    sort_btns:\n      relevance: 関連性\n      newest: 最新\n      active: アクティブ\n      score: スコア\n      more: もっと見る\n    tips:\n      title: 詳細検索のヒント\n      tag: \"<1>[tag]</1> タグで検索\"\n      user: \"<1>ユーザー:ユーザー名</1>作成者による検索\"\n      answer: \"<1>回答:0</1>未回答の質問\"\n      score: \"<1>スコア:3</1>3以上のスコアを持つ投稿\"\n      question: \"<1>質問</1>質問を検索\"\n      is_answer: \"<1>は答え</1>答えを検索\"\n    empty: 何も見つかりませんでした。 <br /> 別のキーワードまたはそれ以下の特定のキーワードをお試しください。\n  share:\n    name: シェア\n    copy: リンクをコピー\n    via: 投稿を共有...\n    copied: コピーしました\n    facebook: Facebookで共有\n    twitter: Xでシェア\n  cannot_vote_for_self: 自分の投稿には投票できません。\n  modal_confirm:\n    title: エラー...\n  delete_permanently:\n    title: 完全に削除する\n    content: 完全に削除しても良いですか？\n  account_result:\n    success: 新しいアカウントが確認されました。ホームページにリダイレクトされます。\n    link: ホームページへ\n    oops: おっと!\n    invalid: 使用したリンクは機能しません。\n    confirm_new_email: メールアドレスが更新されました。\n    confirm_new_email_invalid: >-\n      申し訳ありませんが、この確認リンクは無効です。メールアドレスが既に変更されている可能性があります。\n  unsubscribe:\n    page_title: 購読解除\n    success_title: 購読解除成功\n    success_desc: 配信リストから削除され、その他のメールの送信が停止されました。\n    link: 設定の変更\n  question:\n    following_tags: フォロー中のタグ\n    edit: 編集\n    save: 保存\n    follow_tag_tip: タグに従って質問のリストをキュレートします。\n    hot_questions: ホットな質問\n    all_questions: すべての質問\n    x_questions: \"{{ count }} の質問\"\n    x_answers: \"{{ count }} の回答\"\n    x_posts: \"{{ count }} の回答\"\n    questions: 質問\n    answers: 回答\n    newest: 最新\n    active: 有効\n    hot: 人気\n    frequent: 関心順\n    recommend: おすすめ\n    score: スコア\n    unanswered: 未回答\n    modified: 修正済み\n    answered: 回答済み\n    asked: 質問済み\n    closed: 解決済み\n    follow_a_tag: タグをフォロー\n    more: その他\n  personal:\n    overview: 概要\n    answers: 回答\n    answer: 回答\n    questions: 質問\n    question: 質問\n    bookmarks: ブックマーク\n    reputation: 評判\n    comments: コメント\n    votes: 投票\n    badges: バッジ\n    newest: 最新\n    score: スコア\n    edit_profile: プロファイルを編集\n    visited_x_days: \"{{ count }}人の閲覧者\"\n    viewed: 閲覧回数\n    joined: 参加しました\n    comma: \",\"\n    last_login: 閲覧数\n    about_me: 自己紹介\n    about_me_empty: \"// Hello, World !\"\n    top_answers: よくある回答\n    top_questions: よくある質問\n    stats: 統計情報\n    list_empty: 投稿が見つかりませんでした。<br />他のタブを選択しますか？\n    content_empty: 投稿が見つかりませんでした。\n    accepted: 承認済み\n    answered: 回答済み\n    asked: 質問済み\n    downvoted: 低評価しました\n    mod_short: MOD\n    mod_long: モデレーター\n    x_reputation: 評価\n    x_votes: 投票を受け取りました\n    x_answers: 回答\n    x_questions: 質問\n    recent_badges: 最近のバッジ\n  install:\n    title: Installation\n    next: 次へ\n    done: 完了\n    config_yaml_error: config.yaml を作成できません。\n    lang:\n      label: 言語を選択してください\n    db_type:\n      label: データベースエンジン\n    db_username:\n      label: ユーザー名\n      placeholder: root\n      msg: ユーザー名は空にできません。\n    db_password:\n      label: パスワード\n      placeholder: root\n      msg: パスワードを入力してください。\n    db_host:\n      label: データベースのホスト。\n      placeholder: \"db:3306\"\n      msg: データベースホストは空にできません。\n    db_name:\n      label: データベース名\n      placeholder: 回答\n      msg: データベース名を空にすることはできません。\n    db_file:\n      label: データベースファイル\n      placeholder: /data/answer.db\n      msg: データベースファイルは空にできません。\n    ssl_enabled:\n      label: SSLを有効化\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL モード\n    ssl_root_cert:\n      placeholder: sslrootcert ファイルパス\n      msg: sslrootcert ファイルパスは空にできません\n    ssl_cert:\n      placeholder: sslcert ファイルパス\n      msg: sslcert ファイルパスは空にできません\n    ssl_key:\n      placeholder: sslkey ファイルパス\n      msg: sslkey ファイルパスは空にできません\n    config_yaml:\n      title: config.yamlを作成\n      label: config.yaml ファイルが作成されました。\n      desc: >-\n        <1>/var/www/xxx/</1>ディレクトリに<1>config.yaml</1>ファイルを手動で作成し、その中に次のテキストを貼り付けます。\n      info: 完了したら、「次へ」ボタンをクリックします。\n    site_information: サイト情報\n    admin_account: 管理者アカウント\n    site_name:\n      label: サイト名：\n      msg: サイト名は空にできません．\n      msg_max_length: サイト名は最大30文字でなければなりません。\n    site_url:\n      label: サイトURL\n      text: あなたのサイトのアドレス\n      msg:\n        empty: サイト URL は空にできません．\n        incorrect: サイトURLの形式が正しくありません。\n        max_length: サイトのURLは最大512文字でなければなりません\n    contact_email:\n      label: 連絡先メール アドレス\n      text: このサイトを担当するキーコンタクトのメールアドレスです。\n      msg:\n        empty: 連絡先メールアドレスを空にすることはできません。\n        incorrect: 連絡先メールアドレスの形式が正しくありません。\n    login_required:\n      label: 非公開\n      switch: ログインが必要です\n      text: ログインしているユーザーのみがこのコミュニティにアクセスできます。\n    admin_name:\n      label: 名前\n      msg: 名前を空にすることはできません。\n      character: '使用可能な文字は、英小字「a-z」、数字「0-9」、記号「- . _ 」のみです'\n      msg_max_length: 名前は2 ~ 30文字で入力してください。\n    admin_password:\n      label: パスワード\n      text: >-\n        ログインするにはこのパスワードが必要です。安全な場所に保存してください。\n      msg: パスワードは空白にできません\n      msg_min_length: パスワードは8文字以上でなければなりません。\n      msg_max_length: パスワードは最大 32 文字でなければなりません。\n    admin_confirm_password:\n      label: \"パスワードの確認\"\n      text: \"確認のため、パスワードを再入力してください。\"\n      msg: \"確認用パスワードが一致しません\"\n    admin_email:\n      label: メールアドレス\n      text: ログインするにはこのメールアドレスが必要です。\n      msg:\n        empty: メールアドレスは空にできません。\n        incorrect: メールアドレスの形式が正しくありません.\n    ready_title: サイトの準備ができました\n    ready_desc: >-\n      もっと設定を変更したいと思ったことがある場合は、<1>管理者セクション</1>をご覧ください。サイトメニューで見つけてください。\n    good_luck: \"楽しんで、幸運を!\"\n    warn_title: 警告\n    warn_desc: >-\n      ファイル<1>config.yaml</1>は既に存在します。このファイルのいずれかの設定アイテムをリセットする必要がある場合は、最初に削除してください。\n    install_now: <1>今すぐインストール</1>を試してみてください。\n    installed: 既にインストール済みです\n    installed_desc: >-\n      既にインストールされているようです。再インストールするには、最初に古いデータベーステーブルをクリアしてください。\n    db_failed: データベースの接続が失敗しました\n    db_failed_desc: >-\n      これは、<1>設定内のデータベース情報を意味します。 aml</1>ファイルが正しくないか、データベースサーバーとの連絡先が確立できませんでした。ホストのデータベースサーバーがダウンしている可能性があります。\n  counts:\n    views: ビュー\n    votes: 投票数\n    answers: 回答\n    accepted: 承認済み\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: このページにアクセスする権限がありません。\n    desc_404: 残念ながら、このページは存在しません。\n    desc_50X: サーバーにエラーが発生し、リクエストを完了できませんでした。\n    back_home: ホームページに戻ります\n  page_maintenance:\n    desc: \"メンテナンス中です。まもなく戻ります。\"\n  nav_menus:\n    dashboard: ダッシュボード\n    contents: コンテンツ\n    questions: 質問\n    answers: 回答\n    users: ユーザー\n    badges: バッジ\n    flags: フラグ\n    settings: 設定\n    general: 一般\n    interface: 外観\n    smtp: SMTP\n    branding: ブランディング\n    legal: 法的事項\n    write: 編集\n    terms: 規約\n    tos: 利用規約\n    privacy: プライバシー\n    seo: SEO\n    customize: カスタマイズ\n    themes: テーマ\n    login: ログイン\n    privileges: 特典\n    plugins: プラグイン\n    installed_plugins: 使用中のプラグイン\n    apperance: 外観\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: '{{site_name}} へようこそ'\n  user_center:\n    login: ログイン\n    qrcode_login_tip: QRコードをスキャンしてログインするには {{ agentName }} を使用してください。\n    login_failed_email_tip: ログインに失敗しました。もう一度やり直す前に、このアプリがあなたのメール情報にアクセスすることを許可してください。\n  badges:\n    modal:\n      title: お疲れ様でした！\n      content: 新しいバッジを獲得しました。\n      close: クローズ\n      confirm: バッジを表示\n    title: バッジ\n    awarded: 受賞済み\n    earned_×: 獲得×{{ number }}\n    ×_awarded: \"{{ number }} 受賞\"\n    can_earn_multiple: これを複数回獲得できます。\n    earned: 獲得済み\n  admin:\n    admin_header:\n      title: 管理者\n    dashboard:\n      title: ダッシュボード\n      welcome: Adminへようこそ！\n      site_statistics: サイト統計\n      questions: \"質問：\"\n      resolved: \"解決済み:\"\n      unanswered: \"未回答:\"\n      answers: \"回答：\"\n      comments: \"評論：\"\n      votes: \"投票：\"\n      users: \"ユーザー数:\"\n      flags: \"フラグ:\"\n      reviews: \"レビュー:\"\n      site_health: サイトの状態\n      version: \"バージョン:\"\n      https: \"HTTPS：\"\n      upload_folder: \"フォルダを上げる\"\n      run_mode: \"実行中モード:\"\n      private: 非公開\n      public: 公開\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: システム情報\n      go_version: \"バージョン：\"\n      database: \"データベース：\"\n      database_size: \"データベースのサイズ：\"\n      storage_used: \"使用されているストレージ\"\n      uptime: \"稼働時間:\"\n      links: リンク\n      plugins: プラグイン\n      github: GitHub\n      blog: ブログ\n      contact: 連絡先\n      forum: Forum\n      documents: ドキュメント\n      feedback: フィードバック\n      support: サポート\n      review: レビュー\n      config: 設定\n      update_to: 更新日時\n      latest: 最新\n      check_failed: チェックに失敗しました\n      \"yes\": \"はい\"\n      \"no\": \"いいえ\"\n      not_allowed: 許可されていません\n      allowed: 許可\n      enabled: 有効\n      disabled: 無効\n      writable: 書き込み可\n      not_writable: 書き込み不可\n    flags:\n      title: フラグ\n      pending: 処理待ち\n      completed: 完了済\n      flagged: フラグ付き\n      flagged_type: フラグを立てた {{ type }}\n      created: 作成\n      action: 動作\n      review: レビュー\n    user_role_modal:\n      title: ユーザーロールを変更...\n      btn_cancel: キャンセル\n      btn_submit: 送信\n    new_password_modal:\n      title: 新しいパスワードを設定\n      form:\n        fields:\n          password:\n            label: パスワード\n            text: ユーザーはログアウトされ、再度ログインする必要があります。\n            msg: パスワードの長さは 8 ～ 32 文字である必要があります。\n      btn_cancel: キャンセル\n      btn_submit: 送信\n    edit_profile_modal:\n      title: プロファイルを編集\n      form:\n        fields:\n          display_name:\n            label: 表示名\n            msg_range: 表示名は 2 ~ 30 文字で入力してください。\n          username:\n            label: ユーザー名\n            msg_range: ユーザー名は 2 ~ 30 文字で入力してください。\n          email:\n            label: メールアドレス\n            msg_invalid: 無効なメールアドレス\n      edit_success: 更新が成功しました\n      btn_cancel: キャンセル\n      btn_submit: 送信\n    user_modal:\n      title: 新しいユーザーを追加\n      form:\n        fields:\n          users:\n            label: ユーザーを一括追加\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: '「名前、メールアドレス、パスワード」をカンマで区切ってください。'\n            msg: \"ユーザーのメールアドレスを1行に1つ入力してください。\"\n          display_name:\n            label: 表示名\n            msg: 表示名の長さは 2 ～ 30 文字にする必要があります。\n          email:\n            label: メールアドレス\n            msg: メールアドレスが無効です。\n          password:\n            label: パスワード\n            msg: パスワードの長さは 8 ～ 32 文字である必要があります。\n      btn_cancel: キャンセル\n      btn_submit: 送信\n    users:\n      title: ユーザー\n      name: 名前\n      email: メールアドレス\n      reputation: 評価\n      created_at: 作成日時\n      delete_at: 削除日時\n      suspend_at: 凍結時間\n      suspend_until: まで凍結\n      status: ステータス\n      role: ロール\n      action: 操作\n      change: 変更\n      all: すべて\n      staff: スタッフ\n      more: もっと見る\n      inactive: 非アクティブ\n      suspended: 凍結済み\n      deleted: 削除済み\n      normal: 通常\n      Moderator: モデレーター\n      Admin: 管理者\n      User: ユーザー\n      filter:\n        placeholder: \"ユーザー名でフィルタ\"\n      set_new_password: 新しいパスワードを設定します。\n      edit_profile: プロファイルを編集\n      change_status: ステータスを変更\n      change_role: ロールを変更\n      show_logs: ログを表示\n      add_user: ユーザを追加\n      deactivate_user:\n        title: ユーザーを非アクティブにする\n        content: アクティブでないユーザーはメールアドレスを再認証する必要があります。\n      delete_user:\n        title: このユーザの削除\n        content: このユーザーを削除してもよろしいですか？これは永久的です！\n        remove: このコンテンツを削除\n        label: すべての質問、回答、コメントなどを削除\n        text: ユーザーのアカウントのみ削除したい場合は、これを確認しないでください。\n      suspend_user:\n        title: ユーザーをサスペンドにする\n        content: 一時停止中のユーザーはログインできません。\n        label: いつまで凍結しますか？\n        forever: 無期限\n    questions:\n      page_title: 質問\n      unlisted: 限定公開済み\n      post: 投稿\n      votes: 投票\n      answers: 回答\n      created: 作成\n      status: ステータス\n      action: 動作\n      change: 変更\n      pending: 処理待ち\n      filter:\n        placeholder: \"タイトル、質問:id でフィルター\"\n    answers:\n      page_title: 回答\n      post: 投稿\n      votes: 投票\n      created: 作成\n      status: ステータス\n      action: 操作\n      change: 変更\n      filter:\n        placeholder: \"タイトル、質問:id でフィルター\"\n    general:\n      page_title: 一般\n      name:\n        label: サイト名\n        msg: サイト名は空にできません．\n        text: \"タイトルタグで使用されるこのサイトの名前。\"\n      site_url:\n        label: サイトURL\n        msg: サイト URL は空にできません．\n        validate: 正しいURLを入力してください。\n        text: あなたのサイトのアドレス\n      short_desc:\n        label: 短いサイトの説明\n        msg: 短いサイト説明は空にできません．\n        text: \"ホームページのタイトルタグで使用されている簡単な説明。\"\n      desc:\n        label: サイトの説明\n        msg: サイト説明を空にすることはできません。\n        text: \"メタ説明タグで使用されるように、このサイトを1つの文で説明します。\"\n      contact_email:\n        label: 連絡先メール アドレス\n        msg: 連絡先メールアドレスを空にすることはできません。\n        validate: 連絡先のメールアドレスが無効です。\n        text: このサイトを担当するキーコンタクトのメールアドレスです。\n      check_update:\n        label: ソフトウェアアップデート\n        text: 自動的に更新を確認\n    interface:\n      page_title: 外観\n      language:\n        label: インタフェース言語\n        msg: インターフェース言語は空にできません。\n        text: ユーザーインターフェイスの言語。ページを更新すると変更されます。\n      time_zone:\n        label: タイムゾーン\n        msg: タイムゾーンを空にすることはできません。\n        text: あなたのタイムゾーンを選択してください。\n      avatar:\n        label: デフォルトのアバター\n        text: 独自のカスタムアバターを持たないユーザー向け。\n      gravatar_base_url:\n        label: GravatarのベースURL\n        text: GravatarプロバイダーのAPIベースのURL。空の場合は無視されます。\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: 差出人\n        msg: 差出人メールアドレスは空にできません。\n        text: 送信元のメールアドレス\n      from_name:\n        label: 差出人名\n        msg: 差出人名は空にできません\n        text: メールの送信元の名前\n      smtp_host:\n        label: SMTP ホスト\n        msg: SMTPホストは空にできません。\n        text: メールサーバー\n      encryption:\n        label: 暗号化\n        msg: 暗号化は空にできません。\n        text: ほとんどのサーバではSSLが推奨されます。\n        ssl: SSL\n        tls: TLS\n        none: なし\n      smtp_port:\n        label: SMTPポート\n        msg: SMTPポートは1〜65535でなければなりません。\n        text: メールサーバーへのポート番号\n      smtp_username:\n        label: SMTPユーザ名\n        msg: SMTP ユーザー名を空にすることはできません。\n      smtp_password:\n        label: SMTPパスワード\n        msg: SMTP パスワードを入力してください。\n      test_email_recipient:\n        label: テストメールの受信者\n        text: テスト送信を受信するメールアドレスを入力してください。\n        msg: テストメールの受信者が無効です\n      smtp_authentication:\n        label: 認証を有効にする\n        title: SMTP認証\n        msg: SMTP認証は空にできません。\n        \"yes\": \"はい\"\n        \"no\": \"いいえ\"\n    branding:\n      page_title: ブランディング\n      logo:\n        label: ロゴ\n        msg: ロゴは空にできません。\n        text: あなたのサイトの左上にあるロゴ画像。 高さが56、アスペクト比が3:1を超える広い矩形画像を使用します。 空白の場合、サイトタイトルテキストが表示されます。\n      mobile_logo:\n        label: モバイルロゴ\n        text: サイトのモバイル版で使用されるロゴです。高さが56の横長の長方形の画像を使用してください。空白のままにすると、\"ロゴ\"設定の画像が使用されます。\n      square_icon:\n        label: アイコン画像\n        msg: アイコン画像は空にできません。\n        text: メタデータアイコンのベースとして使用される画像。理想的には512x512より大きくなければなりません。\n      favicon:\n        label: Favicon\n        text: あなたのサイトのファビコン。CDN上で正しく動作するにはpngである必要があります。 32x32にリサイズされます。空白の場合は、\"正方形のアイコン\"が使用されます。\n    legal:\n      page_title: 法的情報\n      terms_of_service:\n        label: 利用規約\n        text: \"ここで利用規約のサービスコンテンツを追加できます。すでに他の場所でホストされているドキュメントがある場合は、こちらにフルURLを入力してください。\"\n      privacy_policy:\n        label: プライバシーポリシー\n        text: \"ここにプライバシーポリシーの内容を追加できます。すでに他の場所でホストされているドキュメントを持っている場合は、こちらにフルURLを入力してください。\"\n      external_content_display:\n        label: 外部コンテンツ\n        text: \"コンテンツには、外部ウェブサイトから埋め込まれた画像、ビデオ、およびメディアが含まれます\"\n        always_display: 常に外部コンテンツを表示する\n        ask_before_display: 外部コンテンツを表示する前に確認する\n    write:\n      page_title: Files\n      min_content:\n        label: 質問に必要な文字数\n        text: 質問の投稿に必要な本文の文字数です。\n      restrict_answer:\n        title: 回答を書く\n        label: 各ユーザーは同じ質問に対して1つの回答しか書けません\n        text: \"ユーザが同じ質問に複数の回答を書き込めるようにするにはオフにします。これにより回答がフォーカスされていない可能性があります。\"\n      min_tags:\n        label: \"質問に必要なタグ数\"\n        text: \"質問の投稿に必要なタグの数です。\"\n      recommend_tags:\n        label: おすすめタグ\n        text: \"デフォルトでドロップダウンリストに推奨タグが表示されます。\"\n        msg:\n          contain_reserved: \"推奨されるタグは予約済みタグを含めることはできません\"\n      required_tag:\n        title: 必須タグを設定\n        label: 必須タグに「推奨タグ」を設定\n        text: \"新しい質問には少なくとも1つの推奨タグが必要です。\"\n      reserved_tags:\n        label: 予約済みタグ\n        text: \"予約済みのタグはモデレータのみ使用できます。\"\n      image_size:\n        label: 画像ファイルの最大サイズ（MB）\n        text: \"画像ファイルの最大アップロードサイズ。\"\n      attachment_size:\n        label: 添付ファイルの最大サイズ (MB)\n        text: \"添付ファイルの最大アップロードサイズ。\"\n      image_megapixels:\n        label: 画像の最大解像度\n        text: \"画像ファイルに許可する最大メガピクセル数。\"\n      image_extensions:\n        label: 認可された画像ファイルの拡張子\n        text: \"イメージ表示に許可されるファイル拡張子のリスト(コンマで区切り)\"\n      attachment_extensions:\n        label: 認可された添付ファイルの拡張子\n        text: \"アップロードを許可するファイル拡張子のリスト(カンマ区切り)\\n警告: アップロードを許可するとセキュリティ上の問題が発生する可能性があります。\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: 固定リンク\n        text: カスタム URL 構造は、ユーザビリティとリンクの前方互換性を向上させることができます。\n      robots:\n        label: robots.txt\n        text: これにより、関連するサイト設定が永久に上書きされます。\n    themes:\n      page_title: テーマ\n      themes:\n        label: テーマ\n        text: 既存のテーマを選択してください\n      color_scheme:\n        label: 配色\n      navbar_style:\n        label: ナビゲーションバーの背景スタイル\n      primary_color:\n        label: メインカラー\n        text: テーマで使用される色を変更する\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS と HTML\n      custom_css:\n        label: カスタム CSS\n        text: >\n\n      head:\n        label: ヘッド\n        text: >\n\n      header:\n        label: ヘッダー\n        text: >\n\n      footer:\n        label: フッター\n        text: これは &lt;/body> の前に挿入されます。\n      sidebar:\n        label: サイドバー\n        text: サイドバーに挿入されます。\n    login:\n      page_title: ログイン\n      membership:\n        title: メンバー\n        label: 新しい登録を許可する\n        text: 誰もが新しいアカウントを作成できないようにするには、オフにしてください。\n      email_registration:\n        title: メールアドレスの登録\n        label: メールアドレスの登録を許可\n        text: オフにすると、メールで新しいアカウントを作成できなくなります。\n      allowed_email_domains:\n        title: 許可されたメールドメイン\n        text: ユーザーがアカウントを登録する必要があるメールドメインです。1行に1つのドメインがあります。空の場合は無視されます。\n      private:\n        title: 非公開\n        label: ログインが必要です\n        text: ログインしているユーザーのみがこのコミュニティにアクセスできます。\n      password_login:\n        title: パスワードログイン\n        label: メールアドレスとパスワードのログインを許可する\n        text: \"警告: オフにすると、他のログイン方法を設定していない場合はログインできない可能性があります。\"\n    installed_plugins:\n      title: インストール済みプラグイン\n      plugin_link: プラグインは機能を拡張します。<1>プラグインリポジトリ</1>にプラグインがあります。\n      filter:\n        all: すべて\n        active: アクティブ\n        inactive: 非アクティブ\n        outdated: 期限切れ\n      plugins:\n        label: プラグイン\n        text: 既存のプラグインを選択します\n      name: 名前\n      version: バージョン\n      status: ステータス\n      action: 操作\n      deactivate: 非アクティブ化\n      activate: アクティベート\n      settings: 設定\n    settings_users:\n      title: ユーザー\n      avatar:\n        label: デフォルトのアバター\n        text: 自分のカスタムアバターのないユーザー向け。\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: GravatarプロバイダーのAPIベースのURL。空の場合は無視されます。\n      profile_editable:\n        title: プロフィール編集可能\n      allow_update_display_name:\n        label: ユーザーが表示名を変更できるようにする\n      allow_update_username:\n        label: ユーザー名の変更を許可する\n      allow_update_avatar:\n        label: ユーザーのプロフィール画像の変更を許可する\n      allow_update_bio:\n        label: ユーザーが自分についての変更を許可する\n      allow_update_website:\n        label: ユーザーのウェブサイトの変更を許可する\n      allow_update_location:\n        label: ユーザーの位置情報の変更を許可する\n    privilege:\n      title: 特権\n      level:\n        label: 評判の必要レベル\n        text: 特権に必要な評判を選択します\n      msg:\n        should_be_number: 入力は数値でなければなりません\n        number_larger_1: 数値は 1 以上でなければなりません\n    badges:\n      action: 操作\n      active: アクティブ\n      activate: アクティベート\n      all: すべて\n      awards: 賞\n      deactivate: 非アクティブ化\n      filter:\n        placeholder: 名前、バッジ:id でフィルター\n      group: グループ\n      inactive: 非アクティブ\n      name: 名前\n      show_logs: ログを表示\n      status: ステータス\n      title: バッジ\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (任意)\n    empty: 空にすることはできません\n    invalid: 無効です\n    btn_submit: 保存\n    not_found_props: \"必須プロパティ {{ key }} が見つかりません。\"\n    select: 選択\n  page_review:\n    review: レビュー\n    proposed: 提案された\n    question_edit: 質問の編集\n    answer_edit: 回答の編集\n    tag_edit: タグの編集\n    edit_summary: 概要を編集\n    edit_question: 質問を編集\n    edit_answer: 回答を編集\n    edit_tag: タグを編集\n    empty: レビュータスクは残っていません。\n    approve_revision_tip: この修正を承認しますか？\n    approve_flag_tip: このフラグを承認しますか？\n    approve_post_tip: この投稿を承認しますか？\n    approve_user_tip: このユーザーを承認しますか？\n    suggest_edits: 提案された編集\n    flag_post: 報告された投稿\n    flag_user: 報告されたユーザー\n    queued_post: キューに入れられた投稿\n    queued_user: キューに入れられたユーザー\n    filter_label: タイプ\n    reputation: 評価\n    flag_post_type: この投稿は {{ type }} として報告されました\n    flag_user_type: このユーザーは {{ type }} として報告されました\n    edit_post: 投稿を編集\n    list_post: 投稿一覧\n    unlist_post: 限定公開投稿\n  timeline:\n    undeleted: 復元する\n    deleted: 削除済み\n    downvote: 低評価\n    upvote: 高評価\n    accept: 承認\n    cancelled: キャンセル済み\n    commented: コメントしました\n    rollback: rollback\n    edited: 編集済み\n    answered: 回答済み\n    asked: 質問済み\n    closed: クローズ済み\n    reopened: 再オープン\n    created: 作成済み\n    pin: ピン留め済\n    unpin: ピン留め解除\n    show: 限定公開解除済み\n    hide: 限定公開済み\n    title: \"履歴：\"\n    tag_title: \"タイムライン：\"\n    show_votes: \"投票を表示\"\n    n_or_a: N/A\n    title_for_question: \"タイムライン：\"\n    title_for_answer: \"{{ title }} の {{ author }} 回答のタイムライン\"\n    title_for_tag: \"タグのタイムライン：\"\n    datetime: 日付時刻\n    type: タイプ\n    by: By\n    comment: コメント\n    no_data: \"何も見つけられませんでした\"\n  users:\n    title: ユーザー\n    users_with_the_most_reputation: 今週最も高い評価スコアを持つユーザ\n    users_with_the_most_vote: 今週一番多く投票したユーザー\n    staffs: コミュニティのスタッフ\n    reputation: 評価\n    votes: 投票\n  prompt:\n    leave_page: このページから移動してもよろしいですか？\n    changes_not_save: 変更が保存されない可能性があります\n  draft:\n    discard_confirm: 下書きを破棄してもよろしいですか？\n  messages:\n    post_deleted: この投稿は削除されました。\n    post_cancel_deleted: この投稿の削除が取り消されました。\n    post_pin: この投稿はピン留めされています。\n    post_unpin: この投稿のピン留めが解除されました。\n    post_hide_list: この投稿は一覧から非表示になっています。\n    post_show_list: この投稿は一覧に表示されています。\n    post_reopen: この投稿は再オープンされました。\n    post_list: この投稿は一覧に表示されています。\n    post_unlist: この投稿は一覧に登録されていません。\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: この投稿はクローズされました。\n    answer_deleted: この回答は削除されました。\n    answer_cancel_deleted: この回答の削除が取り消されました。\n    change_user_role: このユーザーのロールが変更されました。\n    user_inactive: このユーザーは既に無効です。\n    user_normal: このユーザーは既に有効です。\n    user_suspended: このユーザーは凍結されています。\n    user_deleted: このユーザーは削除されました。\n    user_added: User has been added successfully.\n    badge_activated: このバッジは有効化されました。\n    badge_inactivated: このバッジは無効化されています。\n    users_deleted: このユーザーは削除されました。\n    posts_deleted: この質問は削除されています。\n    answers_deleted: この回答は削除されています。\n    copy: クリップボードにコピー\n    copied: コピーしました\n    external_content_warning: 外部の画像/メディアは表示されません。\n\n\n"
  },
  {
    "path": "i18n/ko_KR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: 성공.\n    unknown:\n      other: 알 수 없는 오류.\n    request_format_error:\n      other: 요청 형식이 잘못되었습니다.\n    unauthorized_error:\n      other: 권한이 없습니다.\n    database_error:\n      other: 데이터 서버 오류.\n    forbidden_error:\n      other: 접근 금지.\n    duplicate_request_error:\n      other: 중복된 제출입니다.\n  action:\n    report:\n      other: 신고하기\n    edit:\n      other: 편집\n    delete:\n      other: 삭제\n    close:\n      other: 닫기\n    reopen:\n      other: 다시 열기\n    forbidden_error:\n      other: 접근 금지.\n    pin:\n      other: 고정\n    hide:\n      other: 리스트 제거\n    unpin:\n      other: 고정 해제\n    show:\n      other: 리스트\n    invite_someone_to_answer:\n      other: 편집\n    undelete:\n      other: 삭제 취소\n    merge:\n      other: 병합\n  role:\n    name:\n      user:\n        other: 유저\n      admin:\n        other: 관리자\n      moderator:\n        other: 중재자\n    description:\n      user:\n        other: 특별한 접근 권한이 없는 기본 사용자입니다.\n      admin:\n        other: 사이트에 대한 모든 권한을 가집니다.\n      moderator:\n        other: 관리자 설정을 제외한 모든 게시물에 접근할 수 있습니다.\n  privilege:\n    level_1:\n      description:\n        other: 레벨 1 (비공개 팀, 그룹에 적은 평판 필요)\n    level_2:\n      description:\n        other: 레벨 2 (스타트업 커뮤니티에 낮은 평판 필요)\n    level_3:\n      description:\n        other: 레벨 3 (성숙한 커뮤니티에 높은 평판 필요)\n    level_custom:\n      description:\n        other: 커스텀 레벨\n    rank_question_add_label:\n      other: 질문하기\n    rank_answer_add_label:\n      other: 답변하기\n    rank_comment_add_label:\n      other: 댓글 작성하기\n    rank_report_add_label:\n      other: 신고하기\n    rank_comment_vote_up_label:\n      other: 댓글 추천\n    rank_link_url_limit_label:\n      other: 한번에 2개 이상의 링크를 게시하세요\n    rank_question_vote_up_label:\n      other: 질문 추천\n    rank_answer_vote_up_label:\n      other: 답변 추천\n    rank_question_vote_down_label:\n      other: 질문 비추천\n    rank_answer_vote_down_label:\n      other: 답변 비추천\n    rank_invite_someone_to_answer_label:\n      other: 답변할 사람 초대\n    rank_tag_add_label:\n      other: 새 태그 만들기\n    rank_tag_edit_label:\n      other: 태그 설명 편집 (검토 필요)\n    rank_question_edit_label:\n      other: 다른 사람의 질문 편집 (검토 필요)\n    rank_answer_edit_label:\n      other: 다른 사람의 답변 편집 (검토 필요)\n    rank_question_edit_without_review_label:\n      other: 검토 없이 다른 사람의 질문 편집\n    rank_answer_edit_without_review_label:\n      other: 검토 없이 다른 사람의 답변 편집\n    rank_question_audit_label:\n      other: 질문 편집 검토하기\n    rank_answer_audit_label:\n      other: 답변 편집 검토하기\n    rank_tag_audit_label:\n      other: 태그 편집 검토하기\n    rank_tag_edit_without_review_label:\n      other: 검토 없이 태그 설명 편집\n    rank_tag_synonym_label:\n      other: 태그 동의어 관리\n  email:\n    other: 이메일\n  e_mail:\n    other: 이메일\n  password:\n    other: 비밀번호\n  pass:\n    other: 비밀번호\n  old_pass:\n    other: 현재 비밀번호\n  original_text:\n    other: 이 게시물\n  email_or_password_wrong_error:\n    other: 이메일과 비밀번호가 일치하지 않습니다.\n  error:\n    common:\n      invalid_url:\n        other: 잘못된 URL 입니다.\n      status_invalid:\n        other: 유효하지 않은 상태.\n    password:\n      space_invalid:\n        other: 암호에는 공백을 포함할 수 없습니다.\n    admin:\n      cannot_update_their_password:\n        other: 암호를 변경할 수 없습니다.\n      cannot_edit_their_profile:\n        other: 수정할 수 없습니다.\n      cannot_modify_self_status:\n        other: 상태를 변경할 수 없습니다.\n      email_or_password_wrong:\n        other: 이메일과 비밀번호가 일치하지 않습니다.\n    answer:\n      not_found:\n        other: 답변을 찾을 수 없습니다.\n      cannot_deleted:\n        other: 삭제 권한이 없습니다.\n      cannot_update:\n        other: 편집 권한이 없습니다.\n      question_closed_cannot_add:\n        other: 질문이 닫혔으며, 추가할 수 없습니다.\n      content_cannot_empty:\n        other: 답변 내용은 비워둘 수 없습니다.\n    comment:\n      edit_without_permission:\n        other: 편집이 가능하지 않은 댓글입니다.\n      not_found:\n        other: 댓글을 찾을 수 없습니다.\n      cannot_edit_after_deadline:\n        other: 수정 허용 시간을 초과했습니다.\n      content_cannot_empty:\n        other: 댓글 내용은 비워둘 수 없습니다.\n    email:\n      duplicate:\n        other: 이미 존재하는 이메일입니다.\n      need_to_be_verified:\n        other: 이메일을 확인해주세요.\n      verify_url_expired:\n        other: URL이 만료되었습니다. 이메일을 다시 보내주세요.\n      illegal_email_domain_error:\n        other: 해당 도메인을 사용할 수 없습니다. 다른 전자 메일을 사용하십시오.\n    lang:\n      not_found:\n        other: 언어 파일을 찾을 수 없습니다.\n    object:\n      captcha_verification_failed:\n        other: 잘못된 Captcha입니다.\n      disallow_follow:\n        other: 팔로우할 수 없습니다.\n      disallow_vote:\n        other: 추천할 수 없습니다.\n      disallow_vote_your_self:\n        other: 자신의 게시물에는 추천할 수 없습니다.\n      not_found:\n        other: 오브젝트를 찾을 수 없습니다.\n      verification_failed:\n        other: 확인 실패.\n      email_or_password_incorrect:\n        other: 이메일과 비밀번호가 일치하지 않습니다.\n      old_password_verification_failed:\n        other: 이전 암호 확인에 실패했습니다\n      new_password_same_as_previous_setting:\n        other: 새 비밀번호는 이전 비밀번호와 다르게 입력해주세요.\n      already_deleted:\n        other: 이 게시물은 삭제되었습니다.\n    meta:\n      object_not_found:\n        other: 메타 객체를 찾을 수 없습니다\n    question:\n      already_deleted:\n        other: 삭제된 게시물입니다.\n      under_review:\n        other: 검토를 기다리고 있습니다. 승인된 후에 볼 수 있습니다.\n      not_found:\n        other: 질문을 찾을 수 없습니다.\n      cannot_deleted:\n        other: 삭제 권한이 없습니다.\n      cannot_close:\n        other: 닫을 권한이 없습니다.\n      cannot_update:\n        other: 업데이트 권한이 없습니다.\n      content_cannot_empty:\n        other: 내용은 비워둘 수 없습니다.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: 등급이 조건을 충족하지 못합니다.\n      vote_fail_to_meet_the_condition:\n        other: 피드백 감사합니다. 투표를 하려면 최소 {{.Rank}} 평판이 필요합니다.\n      no_enough_rank_to_operate:\n        other: 이 작업을 하려면 최소 {{.Rank}} 평판이 필요합니다.\n    report:\n      handle_failed:\n        other: 신고 처리에 실패했습니다.\n      not_found:\n        other: 신고를 찾을 수 없습니다.\n    tag:\n      already_exist:\n        other: 이미 존재하는 태그입니다.\n      not_found:\n        other: 태그를 찾을 수 없습니다.\n      recommend_tag_not_found:\n        other: 추천 태그가 존재하지 않습니다.\n      recommend_tag_enter:\n        other: 하나 이상의 태그를 입력해주세요.\n      not_contain_synonym_tags:\n        other: 동일한 태그를 포함하지 않아야 합니다.\n      cannot_update:\n        other: 편집 권한이 없습니다.\n      is_used_cannot_delete:\n        other: 사용 중인 태그는 삭제할 수 없습니다.\n      cannot_set_synonym_as_itself:\n        other: 현재 태그의 동의어로 자기 자신을 설정할 수 없습니다.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: 발신자 이름은 이메일 주소가 될 수 없습니다.\n    theme:\n      not_found:\n        other: 테마를 찾을 수 없습니다.\n    revision:\n      review_underway:\n        other: 검토 대기열에 있기 때문에 현재 편집할 수 없습니다.\n      no_permission:\n        other: 수정권한이 없습니다.\n    user:\n      external_login_missing_user_id:\n        other: 서드파티 플랫폼에서 고유 사용자 ID를 제공하지 않아 로그인할 수 없습니다. 웹사이트 관리자에게 문의하세요.\n      external_login_unbinding_forbidden:\n        other: 이 로그인을 제거하기 전에 계정에 로그인 비밀번호를 설정하세요.\n      email_or_password_wrong:\n        other:\n          other: 이메일 또는 비밀번호가 일치하지 않습니다.\n      not_found:\n        other: 사용자를 찾을 수 없습니다.\n      suspended:\n        other: 사용자가 정지되었습니다.\n      username_invalid:\n        other: 유효하지 않은 이름입니다.\n      username_duplicate:\n        other: 이미 사용중인 이름입니다.\n      set_avatar:\n        other: 아바타 설정에 실패했습니다.\n      cannot_update_your_role:\n        other: 수정할 수 없습니다.\n      not_allowed_registration:\n        other: 현재 사이트는 등록할 수 없습니다.\n      not_allowed_login_via_password:\n        other: 현재 사이트는 비밀번호를 통해 로그인할 수 없습니다.\n      access_denied:\n        other: 접근이 거부당했습니다.\n      page_access_denied:\n        other: 이 페이지에 대한 접근 권한이 없습니다.\n      add_bulk_users_format_error:\n        other: \"줄 {{.Line}}의 '{{.Content}}' 근처 {{.Field}} 형식 오류입니다. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"한 번에 추가할 수 있는 사용자 수는 1-{{.MaxAmount}} 범위 내에 있어야 합니다.\"\n      status_suspended_forever:\n        other: \"<strong>이 사용자는 무기한 접근이 금지 되었습니다.</strong> 이 사용자는 커뮤니티의 지침을 충족시키지 않았습니다.\"\n      status_suspended_until:\n        other: \"<strong>이 사용자는 {{.SuspendedUntil}} 까지 접근이 금지 되었습니다.</strong> 이 사용자는 커뮤니티의 지침을 충족시키지 않았습니다.\"\n      status_deleted:\n        other: \"삭제된 사용자입니다.\"\n      status_inactive:\n        other: \"비활성 사용자입니다.\"\n    config:\n      read_config_failed:\n        other: 컨피그파일 읽기를 실패했습니다\n    database:\n      connection_failed:\n        other: 데이터베이스 연결을 실패했습니다\n      create_table_failed:\n        other: 테이블 생성을 실패했습니다\n    install:\n      create_config_failed:\n        other: config.yaml 파일을 생성할 수 없습니다.\n    upload:\n      unsupported_file_format:\n        other: 지원하지 않는 파일 형식입니다.\n    site_info:\n      config_not_found:\n        other: 사이트 설정을 찾을 수 없습니다.\n    badge:\n      object_not_found:\n        other: 배지 객체를 찾을 수 없습니다\n  reason:\n    spam:\n      name:\n        other: 스팸\n      desc:\n        other: 이 게시물은 광고로 인식되었습니다. 현재 주제와 관련이 없습니다.\n    rude_or_abusive:\n      name:\n        other: 폭언 또는 무례한 언행입니다.\n      desc:\n        other: \"대화에 부적절한 내용입니다.\"\n    a_duplicate:\n      name:\n        other: 중복\n      desc:\n        other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다.\n      placeholder:\n        other: 이미 존재하는 질문입니다\n    not_a_answer:\n      name:\n        other: 답변이 아닙니다\n      desc:\n        other: \"이 내용은 답변으로 게시 되었지만, 질문에 대한 답변 시도가 아닙니다. 이는 수정, 댓글, 다른 질문으로 올리는 것이 적절하거나, 삭제하는 것이 적절할 수도 있습니다.\"\n    no_longer_needed:\n      name:\n        other: 더 이상 필요하지 않습니다.\n      desc:\n        other: 이 의견은 구식이거나 대화 중이거나 이 게시물과 관련이 없습니다.\n    something:\n      name:\n        other: 다른 항목\n      desc:\n        other: 이 게시물은 위에 나열되지 않은 다른 이유로 직원의 주의가 필요합니다.\n      placeholder:\n        other: 귀하가 우려하는 사항을 구체적으로 알려주세요.\n    community_specific:\n      name:\n        other: 커뮤니티별 특정 이유\n      desc:\n        other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다.\n    not_clarity:\n      name:\n        other: 세부사항 또는 명확성이 필요합니다.\n      desc:\n        other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다.\n    looks_ok:\n      name:\n        other: 괜찮아 보입니다.\n      desc:\n        other: 이 게시물은 괜찮으며 품질이 낮지 않습니다.\n    needs_edit:\n      name:\n        other: 편집이 필요하고, 제가 했습니다.\n      desc:\n        other: 이 게시물에 대한 문제를 직접 개선하고 수정합니다.\n    needs_close:\n      name:\n        other: 닫혀야 함\n      desc:\n        other: 닫힌 질문은 대답할 수 없지만 편집, 투표 및 댓글 작성을 할 수 있습니다.\n    needs_delete:\n      name:\n        other: 삭제해야 함\n      desc:\n        other: 이 게시물은 삭제되었습니다.\n  question:\n    close:\n      duplicate:\n        name:\n          other: 스팸\n        desc:\n          other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다.\n      guideline:\n        name:\n          other: 커뮤니티 특정 이유\n        desc:\n          other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다.\n      multiple:\n        name:\n          other: 세부사항 또는 명확성이 필요합니다.\n        desc:\n          other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다.\n      other:\n        name:\n          other: 다른 이유\n        desc:\n          other: 이 게시물에는 위에 나열되지 않은 다른 이유가 필요합니다.\n    operation_type:\n      asked:\n        other: 질문됨\n      answered:\n        other: 답변됨\n      modified:\n        other: 수정됨\n    deleted_title:\n      other: 이미 삭제된 게시물입니다.\n    questions_title:\n      other: 질문들\n  tag:\n    tags_title:\n      other: 태그\n    no_description:\n      other: 이 태그에는 설명이 없습니다.\n  notification:\n    action:\n      update_question:\n        other: 수정된 질문\n      answer_the_question:\n        other: 대답한 질문\n      update_answer:\n        other: 수정된 대답\n      accept_answer:\n        other: 채택된 답변\n      comment_question:\n        other: 질문에 댓글 달림\n      comment_answer:\n        other: 답변에 댓글 달림\n      reply_to_you:\n        other: 당신에게 답변함\n      mention_you:\n        other: 당신을 언급함\n      your_question_is_closed:\n        other: 당신의 질문이 닫혔습니다\n      your_question_was_deleted:\n        other: 당신의 질문이 삭제되었습니다\n      your_answer_was_deleted:\n        other: 당신의 답변이 삭제되었습니다\n      your_comment_was_deleted:\n        other: 당신의 댓글이 삭제되었습니다\n      up_voted_question:\n        other: 질문에 추천함\n      down_voted_question:\n        other: 질문에 비추천함\n      up_voted_answer:\n        other: 답변에 추천함\n      down_voted_answer:\n        other: 답변에 비추천함\n      up_voted_comment:\n        other: 댓글에 추천함\n      invited_you_to_answer:\n        other: 답변에 초대함\n      earned_badge:\n        other: '\"{{.BadgeName}}\" 배지를 획득했습니다'\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] 새 이메일 주소 확인\"\n      body:\n        other: \"다음 링크를 클릭하여 {{.SiteName}} 의 새 이메일 주소를 확인하세요:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\n이 변경을 요청하지 않았다면 이 이메일을 무시하세요.<br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 님이 답변을 작성했습니다\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>{{.SiteName}} 에서 보기</a><br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>구독 취소</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 님이 답변을 요청했습니다\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>답변을 아실 것 같습니다.</blockquote><br>\\n<a href='{{.InviteUrl}}'>{{.SiteName}} 에서 보기</a><br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>구독 취소</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 님이 당신의 게시물에 댓글을 남겼습니다\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>{{.SiteName}} 에서 보기</a><br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>구독 취소</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] 새 질문: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>구독 취소</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] 비밀번호 재설정\"\n      body:\n        other: \"누군가가 {{.SiteName}} 에서 당신의 비밀번호 재설정을 요청했습니다.<br><br>\\n\\n당신이 요청하지 않았다면 이 이메일을 무시해도 됩니다.<br><br>\\n\\n새 비밀번호를 선택하려면 다음 링크를 클릭하세요:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] 새 계정 확인\"\n      body:\n        other: \"{{.SiteName}} 에 오신 것을 환영합니다!<br><br>\\n\\n새 계정을 확인하고 활성화하려면 다음 링크를 클릭하세요:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\n위 링크가 동작하지 않으면 복사하여 브라우저의 주소 입력에 직접 붙여넣으세요.\\n<br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] 테스트 이메일\"\n      body:\n        other: \"이것은 테스트 이메일입니다.\\n<br><br>\\n\\n--<br>\\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.\"\n  action_activity_type:\n    upvote:\n      other: 추천\n    upvoted:\n      other: 추천함\n    downvote:\n      other: 비추천\n    downvoted:\n      other: 비추천함\n    accept:\n      other: 채택\n    accepted:\n      other: 채택됨\n    edit:\n      other: 수정\n  review:\n    queued_post:\n      other: 대기 중인 게시물\n    flagged_post:\n      other: 신고된 게시물\n    suggested_post_edit:\n      other: 건의된 수정\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} 외 {{ .Count }} 명...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: 자서전 작가\n        desc:\n          other: <a href=\"{{ .ProfileURL }}\" target=\"_blank\">프로필</a> 정보를 작성했습니다.\n      certified:\n        name:\n          other: 인증됨\n        desc:\n          other: 신규 사용자 튜토리얼을 완료했습니다.\n      editor:\n        name:\n          other: 에디터\n        desc:\n          other: 첫 게시물 편집.\n      first_flag:\n        name:\n          other: 첫 번째 플래그\n        desc:\n          other: 첫 번째로 게시물에 플래그를 설정했습니다.\n      first_upvote:\n        name:\n          other: 첫 번째 추천\n        desc:\n          other: 첫 번째로 게시물을 추천했습니다.\n      first_link:\n        name:\n          other: 첫 번째 링크\n        desc:\n          other: 첫 번째로 다른 게시물에 링크를 추가했습니다.\n      first_reaction:\n        name:\n          other: 첫 번째 반응\n        desc:\n          other: 첫 번째로 게시물에 반응했습니다.\n      first_share:\n        name:\n          other: 첫 번째 공유\n        desc:\n          other: 첫 번째로 게시물을 공유했습니다.\n      scholar:\n        name:\n          other: 학자\n        desc:\n          other: 질문을 하고 답변을 채택했습니다.\n      commentator:\n        name:\n          other: 해설자\n        desc:\n          other: 댓글 5 개 남기기.\n      new_user_of_the_month:\n        name:\n          other: 이달의 신규 사용자\n        desc:\n          other: 첫 달의 뛰어난 기여.\n      read_guidelines:\n        name:\n          other: 가이드라인 읽음\n        desc:\n          other: '[커뮤니티 가이드라인] 을 읽어보세요.'\n      reader:\n        name:\n          other: 리더\n        desc:\n          other: 10 개 이상의 답변이 있는 주제의 모든 답변을 읽어보세요.\n      welcome:\n        name:\n          other: 환영합니다\n        desc:\n          other: 추천을 받았습니다.\n      nice_share:\n        name:\n          other: 좋은 공유\n        desc:\n          other: 25 명의 고유 방문자와 게시물을 공유했습니다.\n      good_share:\n        name:\n          other: 더 좋은 공유\n        desc:\n          other: 300 명의 고유 방문자와 게시물을 공유했습니다.\n      great_share:\n        name:\n          other: 훌륭한 공유\n        desc:\n          other: 1000 명의 고유 방문자와 게시물을 공유했습니다.\n      out_of_love:\n        name:\n          other: 사랑이 식어서\n        desc:\n          other: 하루에 50 개의 추천을 사용했습니다.\n      higher_love:\n        name:\n          other: 더 큰 사랑\n        desc:\n          other: 하루에 50 개의 추천을 5 번 사용했습니다.\n      crazy_in_love:\n        name:\n          other: 사랑에 미치다\n        desc:\n          other: 하루에 50 개의 추천을 20 번 사용했습니다.\n      promoter:\n        name:\n          other: 홍보자\n        desc:\n          other: 사용자를 초대했습니다.\n      campaigner:\n        name:\n          other: 캠페인 담당자\n        desc:\n          other: 3 명의 기본 사용자를 초대했습니다.\n      champion:\n        name:\n          other: 챔피언\n        desc:\n          other: 5 명의 멤버를 초대했습니다.\n      thank_you:\n        name:\n          other: 감사합니다\n        desc:\n          other: 20 개의 추천받은 게시물을 보유하고 10 개의 추천을 주었습니다.\n      gives_back:\n        name:\n          other: 보답\n        desc:\n          other: 100 개의 추천받은 게시물을 보유하고 100 개의 추천을 주었습니다.\n      empathetic:\n        name:\n          other: 공감적\n        desc:\n          other: 500 개의 추천받은 게시물을 보유하고 1000 개의 추천을 주었습니다.\n      enthusiast:\n        name:\n          other: 열성팬\n        desc:\n          other: 10 일 연속으로 방문했습니다.\n      aficionado:\n        name:\n          other: 애호가\n        desc:\n          other: 100 일 연속으로 방문했습니다.\n      devotee:\n        name:\n          other: 열성 지지자\n        desc:\n          other: 365 일 연속으로 방문했습니다.\n      anniversary:\n        name:\n          other: 기념일\n        desc:\n          other: 1 년 동안 활발한 멤버로 활동하며 최소 한 번 이상 게시했습니다.\n      appreciated:\n        name:\n          other: 감사합니다\n        desc:\n          other: 20 개의 게시물에서 1 번의 추천을 받았습니다.\n      respected:\n        name:\n          other: 존경받는\n        desc:\n          other: 100 개의 게시물에서 2 번의 추천을 받았습니다.\n      admired:\n        name:\n          other: 존경받는\n        desc:\n          other: 300 개의 게시물에서 5 번의 추천을 받았습니다.\n      solved:\n        name:\n          other: 해결됨\n        desc:\n          other: 답변이 채택되었습니다.\n      guidance_counsellor:\n        name:\n          other: 지도 상담사\n        desc:\n          other: 10 개의 답변이 채택되었습니다.\n      know_it_all:\n        name:\n          other: 만물박사\n        desc:\n          other: 50 개의 답변이 채택되었습니다.\n      solution_institution:\n        name:\n          other: 솔루션 기관\n        desc:\n          other: 150 개의 답변이 채택되었습니다.\n      nice_answer:\n        name:\n          other: 좋은 답변\n        desc:\n          other: 답변 점수가 10 점 이상입니다.\n      good_answer:\n        name:\n          other: 더 좋은 답변\n        desc:\n          other: 답변 점수가 25 점 이상입니다.\n      great_answer:\n        name:\n          other: 훌륭한 답변\n        desc:\n          other: 답변 점수가 50 점 이상입니다.\n      nice_question:\n        name:\n          other: 좋은 질문\n        desc:\n          other: 질문 점수가 10 점 이상입니다.\n      good_question:\n        name:\n          other: 좋은 질문\n        desc:\n          other: 질문 점수가 25 점 이상입니다.\n      great_question:\n        name:\n          other: 훌륭한 질문\n        desc:\n          other: 질문 점수가 50 점 이상입니다.\n      popular_question:\n        name:\n          other: 인기 질문\n        desc:\n          other: 500 회 조회된 질문입니다.\n      notable_question:\n        name:\n          other: 중요 질문\n        desc:\n          other: 1,000 회 조회된 질문입니다.\n      famous_question:\n        name:\n          other: 유명 질문\n        desc:\n          other: 5,000 회 조회된 질문입니다.\n      popular_link:\n        name:\n          other: 인기 링크\n        desc:\n          other: 50 번 클릭된 외부 링크를 게시했습니다.\n      hot_link:\n        name:\n          other: 핫 링크\n        desc:\n          other: 300 번 클릭된 외부 링크를 게시했습니다.\n      famous_link:\n        name:\n          other: 유명한 링크\n        desc:\n          other: 100 번 클릭된 외부 링크를 게시했습니다.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: 시작하기\n      community:\n        name:\n          other: 커뮤니티\n      posting:\n        name:\n          other: 포스팅\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: 포맷 방법\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">게시물 언급: <code>#post_id</code></p></li> <li><p class=\"mb-2\">링크 만들기</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[제목](https://url.com)</code></pre></li><li><p class=\"mb-2\">단락 사이에 줄바꿈 넣기</p></li><li><p class=\"mb-2\"><em>_기울임체_</em> 또는 **<strong>굵게</strong>**</p></li><li><p class=\"mb-2\">코드는 4 칸 들여쓰기</p></li><li><p class=\"mb-2\">줄 시작에 <code>&gt;</code> 를 넣어 인용</p></li><li><p class=\"mb-2\">백틱으로 이스케이프 <code>`_이렇게_`</code></p></li><li><p class=\"mb-2\">백틱 <code>`</code> 으로 코드 펜스 생성</p><pre class=\"mb-0\"><code>```<br/>여기에 코드<br/>```</code></pre></li></ul>\n  pagination:\n    prev: 이전\n    next: 다음\n  page_title:\n    question: 질문\n    questions: 질문들\n    tag: 태그\n    tags: 태그들\n    tag_wiki: 태그 위키\n    create_tag: 태그 생성\n    edit_tag: 태그 수정\n    ask_a_question: 질문 생성\n    edit_question: 질문 수정\n    edit_answer: 답변 수정\n    search: 검색\n    posts_containing: 포함된 게시물\n    settings: 설정\n    notifications: 알림\n    login: 로그인\n    sign_up: 회원 가입\n    account_recovery: 계정 복구\n    account_activation: 계정 활성화\n    confirm_email: 이메일 확인\n    account_suspended: 계정 정지\n    admin: 관리자\n    change_email: 이메일 수정\n    install: 답변 설치\n    upgrade: 답변 업그레이드\n    maintenance: 웹사이트 유지보수\n    users: 사용자\n    oauth_callback: 처리 중\n    http_404: HTTP 오류 404\n    http_50X: HTTP 오류 500\n    http_403: HTTP 오류 403\n    logout: 로그아웃\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: 알림\n    inbox: 받은 편지함\n    achievement: 업적\n    new_alerts: 새로운 알림\n    all_read: 모두 읽음 처리\n    show_more: 더 보기\n    someone: 누군가\n    inbox_type:\n      all: 전체\n      posts: 게시물\n      invites: 초대\n      votes: 투표\n    answer: 답변\n    question: 질문\n    badge_award: 뱃지\n  suspended:\n    title: 계정이 정지되었습니다\n    until_time: \"당신의 계정은 {{ time }}까지 정지되었습니다.\"\n    forever: 이 사용자는 영구 정지되었습니다.\n    end: 커뮤니티 가이드라인을 준수하지 않았습니다.\n    contact_us: 문의하기\n  editor:\n    blockquote:\n      text: 인용구\n    bold:\n      text: 강조\n    chart:\n      text: 차트\n      flow_chart: 플로우 차트\n      sequence_diagram: 시퀀스 다이어그램\n      class_diagram: 클래스 다이어그램\n      state_diagram: 상태 다이어그램\n      entity_relationship_diagram: 엔터티 관계 다이어그램\n      user_defined_diagram: 사용자 정의 다이어그램\n      gantt_chart: 간트 차트\n      pie_chart: 파이 차트\n    code:\n      text: 코드 예시\n      add_code: 코드 예시 추가\n      form:\n        fields:\n          code:\n            label: 코드\n            msg:\n              empty: 코드를 입력하세요.\n          language:\n            label: 언어\n            placeholder: 자동 감지\n      btn_cancel: 취소\n      btn_confirm: 추가\n    formula:\n      text: 수식\n      options:\n        inline: 인라인 수식\n        block: 블록 수식\n    heading:\n      text: 제목\n      options:\n        h1: 제목 1\n        h2: 제목 2\n        h3: 제목 3\n        h4: 제목 4\n        h5: 제목 5\n        h6: 제목 6\n    help:\n      text: 도움말\n    hr:\n      text: 가로규칙\n    image:\n      text: 이미지\n      add_image: 이미지 추가\n      tab_image: 이미지 업로드\n      form_image:\n        fields:\n          file:\n            label: 이미지 파일\n            btn: 이미지 선택\n            msg:\n              empty: 파일을 선택하세요.\n              only_image: 이미지 파일만 허용됩니다.\n              max_size: 파일 크기는 {{size}} MB 를 초과할 수 없습니다.\n          desc:\n            label: 설명\n      tab_url: 이미지 URL\n      form_url:\n        fields:\n          url:\n            label: 이미지 URL\n            msg:\n              empty: 이미지 URL을 입력하세요.\n          name:\n            label: 설명\n      btn_cancel: 취소\n      btn_confirm: 추가\n      uploading: 업로드 중\n    indent:\n      text: 들여쓰기\n    outdent:\n      text: 내어쓰기\n    italic:\n      text: 이탤릭체\n    link:\n      text: 링크\n      add_link: 링크 추가\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL을 입력하세요.\n          name:\n            label: 설명\n      btn_cancel: 취소\n      btn_confirm: 추가\n    ordered_list:\n      text: 번호 매긴 목록\n    unordered_list:\n      text: 글머리 기호 목록\n    table:\n      text: 표\n      heading: 제목\n      cell: 셀\n    file:\n      text: 파일 첨부\n      not_supported: \"해당 파일 형식을 지원하지 않습니다. {{file_type}} 로 다시 시도해 주세요.\"\n      max_size: \"첨부 파일 크기는 {{size}} MB 를 초과할 수 없습니다.\"\n  close_modal:\n    title: 이 게시물을 다음과 같은 이유로 닫습니다...\n    btn_cancel: 취소\n    btn_submit: 제출\n    remark:\n      empty: 비어 있을 수 없습니다.\n    msg:\n      empty: 이유를 선택해 주세요.\n  report_modal:\n    flag_title: 이 게시물을 신고합니다...\n    close_title: 이 게시물을 다음과 같은 이유로 닫습니다...\n    review_question_title: 질문 검토\n    review_answer_title: 답변 검토\n    review_comment_title: 댓글 검토\n    btn_cancel: 취소\n    btn_submit: 제출\n    remark:\n      empty: 비어 있을 수 없습니다.\n    msg:\n      empty: 이유를 선택해 주세요.\n      not_a_url: URL 형식이 올바르지 않습니다.\n      url_not_match: URL 원본이 현재 웹사이트와 일치하지 않습니다.\n  tag_modal:\n    title: 새로운 태그 생성\n    form:\n      fields:\n        display_name:\n          label: 표시 이름\n          msg:\n            empty: 표시 이름을 입력하세요.\n            range: 표시 이름은 최대 35자까지 입력 가능합니다.\n        slug_name:\n          label: URL 슬러그\n          desc: '\"a-z\", \"0-9\", \"+ # - .\" 문자 집합을 사용해야 합니다.'\n          msg:\n            empty: URL 슬러그를 입력하세요.\n            range: URL 슬러그는 최대 35자까지 입력 가능합니다.\n            character: 허용되지 않은 문자 집합이 포함되어 있습니다.'\n        desc:\n          label: 설명\n        revision:\n          label: 개정\n        edit_summary:\n          label: 편집 요약\n          placeholder: >-\n            수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등)\n    btn_cancel: 취소\n    btn_submit: 제출\n    btn_post: 새 태그 게시\n  tag_info:\n    created_at: 생성됨\n    edited_at: 편집됨\n    history: 히스토리\n    synonyms:\n      title: 동의어\n      text: 다음 태그가 다음으로 다시 매핑됩니다\n      empty: 동의어가 없습니다.\n      btn_add: 동의어 추가\n      btn_edit: 편집\n      btn_save: 저장\n    synonyms_text: 다음 태그가 다음으로 다시 매핑됩니다\n    delete:\n      title: 이 태그 삭제\n      tip_with_posts: >-\n        <p><strong>게시물이 있는 태그 삭제</strong>는 허용되지 않습니다.</p> <p>먼저 게시물에서 이 태그를 제거해 주세요.</p>\n      tip_with_synonyms: >-\n        <p><strong>동의어가 있는 태그 삭제</strong>는 허용되지 않습니다.</p> <p>먼저 이 태그에서 동의어를 제거해 주세요.</p>\n      tip: 정말로 삭제하시겠습니까?\n      close: 닫기\n    merge:\n      title: 태그 병합\n      source_tag_title: 원본 태그\n      source_tag_description: 원본 태그와 관련 데이터가 대상 태그로 재매핑됩니다.\n      target_tag_title: 대상 태그\n      target_tag_description: 병합 후 이 두 태그 간 동의어가 생성됩니다.\n      no_results: 일치하는 태그가 없습니다\n      btn_submit: 제출\n      btn_close: 닫기\n  edit_tag:\n    title: 태그 수정\n    default_reason: 태그 수정\n    default_first_reason: 태그 추가\n    btn_save_edits: 수정 저장\n    btn_cancel: 취소\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"YYYY년 M월 D일\"\n    long_date_with_time: \"YYYY년 MMM D일 HH:mm\"\n    now: 방금 전\n    x_seconds_ago: \"{{count}}초 전\"\n    x_minutes_ago: \"{{count}}분 전\"\n    x_hours_ago: \"{{count}}시간 전\"\n    hour: 시간\n    day: 일\n    hours: 시간\n    days: 일\n    month: 월\n    months: 개월\n    year: 년\n  reaction:\n    heart: 하트\n    smile: 스마일\n    frown: 찡그린 표정\n    btn_label: 반응 추가 또는 제거\n    undo_emoji: '{{ emoji }} 반응 취소'\n    react_emoji: '{{ emoji }} 로 반응'\n    unreact_emoji: '{{ emoji }} 반응 취소'\n  comment:\n    btn_add_comment: 댓글 추가\n    reply_to: 답글 달기\n    btn_reply: 답글\n    btn_edit: 수정\n    btn_delete: 삭제\n    btn_flag: 신고\n    btn_save_edits: 수정 저장\n    btn_cancel: 취소\n    show_more: \"{{count}}개의 댓글 더 보기\"\n    tip_question: >-\n      더 많은 정보를 요청하거나 개선을 제안하기 위해 댓글을 사용하세요. 댓글에서 질문에 답변하지는 마세요.\n    tip_answer: >-\n      다른 사용자에게 답변하거나 변경 사항을 알릴 때 댓글을 사용하세요. 새로운 정보를 추가하는 경우에는 게시물을 수정하세요.\n    tip_vote: 게시물에 유용한 정보를 추가합니다.\n  edit_answer:\n    title: 답변 수정\n    default_reason: 답변 수정\n    default_first_reason: 답변 추가\n    form:\n      fields:\n        revision:\n          label: 개정\n        answer:\n          label: 답변\n          feedback:\n            characters: 내용은 최소 6자 이상이어야 합니다.\n        edit_summary:\n          label: 편집 요약\n          placeholder: >-\n            수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등)\n    btn_save_edits: 수정 저장\n    btn_cancel: 취소\n  tags:\n    title: 태그들\n    sort_buttons:\n      popular: 인기순\n      name: 이름\n      newest: 최신순\n    button_follow: 팔로우\n    button_following: 팔로잉 중\n    tag_label: 질문들\n    search_placeholder: 태그 이름으로 필터링\n    no_desc: 이 태그에는 설명이 없습니다.\n    more: 더 보기\n    wiki: 위키\n  ask:\n    title: 질문 생성\n    edit_title: 질문 수정\n    default_reason: 질문 수정\n    default_first_reason: 질문 생성\n    similar_questions: 유사한 질문\n    form:\n      fields:\n        revision:\n          label: 개정\n        title:\n          label: 제목\n          placeholder: 주제는 무엇인가요? 상세하게 작성해주세요.\n          msg:\n            empty: 제목을 입력하세요.\n            range: 제목은 최대 150자까지 입력 가능합니다.\n        body:\n          label: 본문\n          msg:\n            empty: 본문을 입력하세요.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: 태그\n          msg:\n            empty: 태그를 입력하세요.\n        answer:\n          label: 답변\n          msg:\n            empty: 답변을 입력하세요.\n        edit_summary:\n          label: 편집 요약\n          placeholder: >-\n            수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등)\n    btn_post_question: 질문 게시하기\n    btn_save_edits: 수정사항 저장\n    answer_question: 질문에 대한 답변 작성\n    post_question&answer: 질문과 답변 게시하기\n  tag_selector:\n    add_btn: 태그 추가\n    create_btn: 새 태그 생성\n    search_tag: 태그 검색\n    hint: 질문의 주제를 설명하세요. 적어도 하나의 태그가 필요합니다.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: 일치하는 태그가 없습니다.\n    tag_required_text: 필수 태그 (적어도 하나)\n  header:\n    nav:\n      question: 질문\n      tag: 태그\n      user: 사용자\n      badges: 뱃지\n      profile: 프로필\n      setting: 설정\n      logout: 로그아웃\n      admin: 관리자\n      review: 리뷰\n      bookmark: 즐겨찾기\n      moderation: 운영\n    search:\n      placeholder: 검색\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: 변경\n    loading: 로딩 중...\n  pic_auth_code:\n    title: 캡차\n    placeholder: 위의 텍스트를 입력하세요\n    msg:\n      empty: 캡차를 입력하세요.\n  inactive:\n    first: >-\n      거의 다 되었습니다! <bold>{{mail}}</bold>로 활성화 메일을 보냈습니다. 계정을 활성화하려면 메일 안의 지침을 따르세요.\n    info: \"메일이 도착하지 않았다면, 스팸 메일함도 확인해 주세요.\"\n    another: >-\n      <bold>{{mail}}</bold>로 또 다른 활성화 이메일을 보냈습니다. 메일이 도착하는 데 몇 분 정도 걸릴 수 있으니 스팸 메일함도 확인해 주세요.\n    btn_name: 활성화 이메일 재전송\n    change_btn_name: 이메일 변경\n    msg:\n      empty: 비어 있을 수 없습니다.\n    resend_email:\n      url_label: 활성화 이메일을 재전송하시겠습니까?\n      url_text: 위의 활성화 링크를 사용자에게 제공할 수도 있습니다.\n  login:\n    login_to_continue: 계속하려면 로그인하세요\n    info_sign: 계정이 없으신가요? <1>가입하기</1>\n    info_login: 이미 계정이 있으신가요? <1>로그인하기</1>\n    agreements: 가입하면 <1>개인정보 보호 정책</1>과 <3>이용 약관</3>에 동의하게 됩니다.\n    forgot_pass: 비밀번호를 잊으셨나요?\n    name:\n      label: 이름\n      msg:\n        empty: 이름을 입력하세요.\n        range: 이름은 2 자에서 30 자 사이여야 합니다.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: 이메일\n      msg:\n        empty: 이메일을 입력하세요.\n    password:\n      label: 비밀번호\n      msg:\n        empty: 비밀번호를 입력하세요.\n        different: 입력된 비밀번호가 일치하지 않습니다.\n  account_forgot:\n    page_title: 비밀번호를 잊으셨나요?\n    btn_name: 비밀번호 재설정 이메일 보내기\n    send_success: >-\n      <strong>{{mail}}</strong>에 해당하는 계정이 있다면 곧 비밀번호 재설정 방법을 안내하는 이메일을 받으실 수 있습니다.\n    email:\n      label: 이메일\n      msg:\n        empty: 이메일을 입력하세요.\n  change_email:\n    btn_cancel: 취소\n    btn_update: 이메일 주소 업데이트\n    send_success: >-\n      <strong>{{mail}}</strong>에 해당하는 계정이 있다면 곧 이메일 주소 변경 방법을 안내하는 이메일을 받으실 수 있습니다.\n    email:\n      label: 새 이메일\n      msg:\n        empty: 이메일을 입력하세요.\n  oauth:\n    connect: '{{ auth_name }}로 연결'\n    remove: '{{ auth_name }} 연결 해제'\n  oauth_bind_email:\n    subtitle: 계정에 복구 이메일 추가\n    btn_update: 이메일 주소 업데이트\n    email:\n      label: 이메일\n      msg:\n        empty: 이메일을 입력하세요.\n    modal_title: 이미 등록된 이메일\n    modal_content: 이 이메일 주소는 이미 등록되어 있습니다. 기존 계정에 연결하시겠습니까?\n    modal_cancel: 이메일 변경\n    modal_confirm: 기존 계정에 연결하기\n  password_reset:\n    page_title: 비밀번호 재설정\n    btn_name: 비밀번호 재설정\n    reset_success: >-\n      비밀번호가 성공적으로 변경되었습니다. 로그인 페이지로 이동합니다.\n    link_invalid: >-\n      죄송합니다. 이 비밀번호 재설정 링크는 더 이상 유효하지 않습니다. 이미 비밀번호를 재설정하셨을 수 있습니다.\n    to_login: 로그인 페이지로 이동\n    password:\n      label: 비밀번호\n      msg:\n        empty: 비밀번호를 입력하세요.\n        length: 비밀번호는 8자에서 32자 사이여야 합니다.\n        different: 입력한 비밀번호가 일치하지 않습니다.\n    password_confirm:\n      label: 새 비밀번호 확인\n  settings:\n    page_title: 설정\n    goto_modify: 수정으로 이동\n    nav:\n      profile: 프로필\n      notification: 알림\n      account: 계정\n      interface: 인터페이스\n    profile:\n      heading: 프로필\n      btn_name: 저장\n      display_name:\n        label: 표시 이름\n        msg: 표시 이름을 입력하세요.\n        msg_range: 표시 이름은 2-30 자 길이여야 합니다.\n      username:\n        label: 사용자 이름\n        caption: 다른 사용자가 \"@사용자이름\"으로 멘션할 수 있습니다.\n        msg: 사용자 이름을 입력하세요.\n        msg_range: 유저 이름은 2-30 자 길이여야 합니다.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: 프로필 이미지\n        gravatar: Gravatar\n        gravatar_text: Gravatar에서 이미지를 변경할 수 있습니다.\n        custom: 사용자 정의\n        custom_text: 사용자 이미지를 업로드할 수 있습니다.\n        default: 시스템 기본 이미지\n        msg: 프로필 이미지를 업로드하세요.\n      bio:\n        label: 자기 소개\n      website:\n        label: 웹사이트\n        placeholder: \"https://example.com\"\n        msg: 웹사이트 형식이 올바르지 않습니다.\n      location:\n        label: 위치\n        placeholder: \"도시, 국가\"\n    notification:\n      heading: 이메일 알림\n      turn_on: 켜기\n      inbox:\n        label: 받은 편지함 알림\n        description: 질문에 대한 답변, 댓글, 초대 등을 받습니다.\n      all_new_question:\n        label: 모든 새 질문\n        description: 모든 새 질문에 대해 알림을 받습니다. 주당 최대 50개의 질문까지.\n      all_new_question_for_following_tags:\n        label: 팔로우 태그의 모든 새 질문\n        description: 팔로우하는 태그의 새로운 질문에 대해 알림을 받습니다.\n    account:\n      heading: 계정\n      change_email_btn: 이메일 변경\n      change_pass_btn: 비밀번호 변경\n      change_email_info: >-\n        해당 주소로 이메일을 보냈습니다. 확인 지침을 따라주세요.\n      email:\n        label: 이메일\n      new_email:\n        label: 새 이메일\n        msg: 새 이메일을 입력하세요.\n      pass:\n        label: 현재 비밀번호\n        msg: 비밀번호를 입력하세요.\n      password_title: 비밀번호\n      current_pass:\n        label: 현재 비밀번호\n        msg:\n          empty: 현재 비밀번호를 입력하세요.\n          length: 비밀번호는 8자에서 32자 사이여야 합니다.\n          different: 입력한 두 비밀번호가 일치하지 않습니다.\n      new_pass:\n        label: 새 비밀번호\n      pass_confirm:\n        label: 새 비밀번호 확인\n    interface:\n      heading: 인터페이스\n      lang:\n        label: 인터페이스 언어\n        text: 사용자 인터페이스 언어입니다. 페이지를 새로고침하면 변경됩니다.\n    my_logins:\n      title: 내 로그인 정보\n      label: 이 사이트에서 이 계정으로 로그인하거나 가입하세요.\n      modal_title: 로그인 제거\n      modal_content: 이 계정에서 이 로그인을 제거하시겠습니까?\n      modal_confirm_btn: 제거\n      remove_success: 제거되었습니다.\n  toast:\n    update: 업데이트 성공\n    update_password: 비밀번호가 성공적으로 변경되었습니다.\n    flag_success: 신고 감사합니다.\n    forbidden_operate_self: 자신에 대한 작업은 금지되어 있습니다.\n    review: 검토 후에 귀하의 수정 사항이 표시됩니다.\n    sent_success: 전송 성공\n  related_question:\n    title: 관련된 질문\n    answers: 답변\n  linked_question:\n    title: 링크된 질문\n    description: 이 질문을 링크한 질문\n    no_linked_question: 이 질문에 연결된 질문 없음.\n  invite_to_answer:\n    title: 질문자 초대\n    desc: 답변을 알고 있을 것으로 생각되는 사람을 선택하세요.\n    invite: 답변 초대\n    add: 사람 추가\n    search: 사람 검색\n  question_detail:\n    action: 동작\n    created: Created\n    Asked: 질문함\n    asked: 질문 작성\n    update: 수정됨\n    Edited: Edited\n    edit: 편집됨\n    commented: 댓글 작성\n    Views: 조회수\n    Follow: 팔로우\n    Following: 팔로잉 중\n    follow_tip: 이 질문을 팔로우하여 알림을 받으세요.\n    answered: 답변 작성\n    closed_in: 답변 종료\n    show_exist: 기존 질문 표시\n    useful: 유용함\n    question_useful: 유용하고 명확함\n    question_un_useful: 불명확하거나 유용하지 않음\n    question_bookmark: 이 질문 즐겨찾기\n    answer_useful: 유용함\n    answer_un_useful: 유용하지 않음\n    answers:\n      title: 답변\n      score: 점수\n      newest: 최신순\n      oldest: 오래된 순\n      btn_accept: 채택\n      btn_accepted: 채택됨\n    write_answer:\n      title: 당신의 답변\n      edit_answer: 내 답변 편집하기\n      btn_name: 답변 게시하기\n      add_another_answer: 다른 답변 추가\n      confirm_title: 답변 계속하기\n      continue: 계속\n      confirm_info: >-\n        <p>다른 답변을 추가하시겠습니까?</p><p>대신 기존 답변을 향상시키고 개선할 수 있는 수정 링크를 사용할 수 있습니다.</p>\n      empty: 답변을 입력해주세요.\n      characters: 내용은 최소 6자 이상이어야 합니다.\n      tips:\n        header_1: 답변해 주셔서 감사합니다\n        li1_1: <strong>질문에 답변</strong>을 제공하세요. 세부 사항을 설명하고 연구 결과를 공유하세요.\n        li1_2: 발언을 뒷받침하는 자료나 개인적인 경험을 통해 주장을 뒷받침하세요.\n        header_2: 하지만 <strong>피해야 할 것들</strong> ...\n        li2_1: 도움을 요청하거나 해명을 구하거나 다른 답변에 응답하는 것.\n    reopen:\n      confirm_btn: 다시 열기\n      title: 이 게시물 다시 열기\n      content: 정말 다시 열기를 원하시나요?\n    list:\n      confirm_btn: 목록\n      title: 이 게시물 목록에 추가하기\n      content: 정말 목록에 추가하시겠습니까?\n    unlist:\n      confirm_btn: 목록 해제\n      title: 이 게시물 목록에서 제외하기\n      content: 정말 목록에서 제외하시겠습니까?\n    pin:\n      title: 이 게시물 고정하기\n      content: 글로벌로 고정하시겠습니까? 이 게시물은 모든 게시물 목록 상단에 표시됩니다.\n      confirm_btn: 고정하기\n  delete:\n    title: 이 게시물 삭제하기\n    question: >-\n      <p><strong>답변이 있는 질문을 삭제하는 것은 권장하지 않습니다</strong> 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다. </p><p>답변이 있는 질문을 반복적으로 삭제하는 경우 질문 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까?\n    answer_accepted: >-\n      <p><strong>채택된 답변을 삭제하는 것은 권장하지 않습니다</strong> 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다. </p><p>채택된 답변을 반복적으로 삭제하는 경우 답변 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까?\n    other: 정말 삭제하시겠습니까?\n    tip_answer_deleted: 이 답변은 삭제되었습니다.\n    undelete_title: 이 게시물 복구하기\n    undelete_desc: 정말 복구하시겠습니까?\n  btns:\n    confirm: 확인\n    cancel: 취소\n    edit: 편집\n    save: 저장\n    delete: 삭제\n    undelete: 복구\n    list: 목록\n    unlist: 목록 해제\n    unlisted: 목록에서 해제됨\n    login: 로그인\n    signup: 가입하기\n    logout: 로그아웃\n    verify: 확인\n    create: 생성\n    approve: 승인\n    reject: 거부\n    skip: 건너뛰기\n    discard_draft: 임시 저장 삭제\n    pinned: 고정됨\n    all: 모두\n    question: 질문\n    answer: 답변\n    comment: 댓글\n    refresh: 새로 고침\n    resend: 재전송\n    deactivate: 비활성화\n    active: 활성화\n    suspend: 정지\n    unsuspend: 정지 해제\n    close: 닫기\n    reopen: 다시 열기\n    ok: 확인\n    light: 밝게\n    dark: 어둡게\n    system_setting: 시스템 설정\n    default: 기본\n    reset: 재설정\n    tag: 태그\n    post_lowercase: 게시물\n    filter: 필터\n    ignore: 무시\n    submit: 제출\n    normal: 일반\n    closed: 닫힘\n    deleted: 삭제됨\n    deleted_permanently: 영구 삭제\n    pending: 보류 중\n    more: 더 보기\n    view: 보기\n    card: 카드\n    compact: 간단히\n    display_below: 아래에 표시\n    always_display: 항상 표시\n    or: 또는\n    back_sites: 사이트로 돌아가기\n  search:\n    title: 검색 결과\n    keywords: 키워드\n    options: 옵션\n    follow: 팔로우\n    following: 팔로잉 중\n    counts: \"{{count}} 개의 결과\"\n    counts_loading: \"... 개의 결과\"\n    more: 더 보기\n    sort_btns:\n      relevance: 관련성\n      newest: 최신순\n      active: 활성순\n      score: 평점순\n      more: 더 보기\n    tips:\n      title: 고급 검색 팁\n      tag: \"<1>[태그]</1> 태그로 검색\"\n      user: \"<1>user:사용자명</1> 작성자로 검색\"\n      answer: \"<1>answers:0</1> 답변이 없는 질문\"\n      score: \"<1>score:3</1> 평점이 3 이상인 글\"\n      question: \"<1>is:question</1> 질문만 검색\"\n      is_answer: \"<1>is:answer</1> 답변만 검색\"\n    empty: 아무것도 찾지 못했습니다. <br /> 다른 키워드를 사용하거나 덜 구체적인 검색을 시도하세요.\n  share:\n    name: 공유\n    copy: 링크 복사\n    via: 포스트 공유하기...\n    copied: 복사됨\n    facebook: Facebook에 공유\n    twitter: X에 공유하기\n  cannot_vote_for_self: 자신의 글에 투표할 수 없습니다.\n  modal_confirm:\n    title: 오류...\n  delete_permanently:\n    title: 영구 삭제\n    content: 영구적으로 삭제하시겠습니까?\n  account_result:\n    success: 새 계정이 확인되었습니다. 홈페이지로 이동합니다.\n    link: 홈페이지로 이동\n    oops: 이런!\n    invalid: 사용하신 링크가 더 이상 작동하지 않습니다.\n    confirm_new_email: 이메일이 업데이트되었습니다.\n    confirm_new_email_invalid: >-\n      죄송합니다, 이 확인 링크는 더 이상 유효하지 않습니다. 이미 이메일이 변경된 상태일 수 있습니다.\n  unsubscribe:\n    page_title: 구독 해지\n    success_title: 구독 해지 완료\n    success_desc: 이 구독자 목록에서 성공적으로 제거되었으며, 더 이상 우리로부터 어떠한 이메일도 받지 않게 됩니다.\n    link: 설정 변경하기\n  question:\n    following_tags: 팔로우 태그\n    edit: 수정\n    save: 저장\n    follow_tag_tip: 질문 목록을 관리하기 위해 태그를 팔로우하세요.\n    hot_questions: 인기 질문\n    all_questions: 모든 질문\n    x_questions: \"{{ count }} 개의 질문\"\n    x_answers: \"{{ count }} 개의 답변\"\n    x_posts: \"{{ count }} 개의 글\"\n    questions: 질문\n    answers: 답변\n    newest: 최신순\n    active: 활성순\n    hot: 인기\n    frequent: 빈도\n    recommend: 추천\n    score: 평점순\n    unanswered: 답변이 없는 질문\n    modified: 수정됨\n    answered: 답변됨\n    asked: 질문됨\n    closed: 닫힘\n    follow_a_tag: 태그 팔로우하기\n    more: 더 보기\n  personal:\n    overview: 개요\n    answers: 답변\n    answer: 답변\n    questions: 질문\n    question: 질문\n    bookmarks: 즐겨찾기\n    reputation: 평판\n    comments: 댓글\n    votes: 투표\n    badges: 뱃지\n    newest: 최신순\n    score: 평점순\n    edit_profile: 프로필 수정\n    visited_x_days: \"{{ count }} 일 방문함\"\n    viewed: 조회됨\n    joined: 가입일\n    comma: \",\"\n    last_login: 최근 접속\n    about_me: 자기 소개\n    about_me_empty: \"// 안녕하세요, 세상아 !\"\n    top_answers: 최고 답변\n    top_questions: 최고 질문\n    stats: 통계\n    list_empty: 게시물을 찾을 수 없습니다.<br />다른 탭을 선택하실 수 있습니다.\n    content_empty: 게시물을 찾을 수 없습니다.\n    accepted: 채택됨\n    answered: 답변됨\n    asked: 질문됨\n    downvoted: 다운투표됨\n    mod_short: MOD\n    mod_long: 관리자\n    x_reputation: 평판\n    x_votes: 받은 투표\n    x_answers: 답변\n    x_questions: 질문\n    recent_badges: 최근 배지\n  install:\n    title: 설치\n    next: 다음\n    done: 완료\n    config_yaml_error: config.yaml 파일을 생성할 수 없습니다.\n    lang:\n      label: 언어 선택\n    db_type:\n      label: 데이터베이스 엔진\n    db_username:\n      label: 사용자 이름\n      placeholder: root\n      msg: 사용자 이름은 비워둘 수 없습니다.\n    db_password:\n      label: 비밀번호\n      placeholder: root\n      msg: 비밀번호는 비워둘 수 없습니다.\n    db_host:\n      label: 데이터베이스 호스트\n      placeholder: \"db:3306\"\n      msg: 데이터베이스 호스트는 비워둘 수 없습니다.\n    db_name:\n      label: 데이터베이스 이름\n      placeholder: 답변\n      msg: 데이터베이스 이름은 비워둘 수 없습니다.\n    db_file:\n      label: 데이터베이스 파일\n      placeholder: /data/answer.db\n      msg: 데이터베이스 파일은 비워둘 수 없습니다.\n    ssl_enabled:\n      label: SSL 활성화\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL 모드\n    ssl_root_cert:\n      placeholder: sslrootcert 파일 경로\n      msg: sslrootcert 파일 경로는 비워둘 수 없습니다\n    ssl_cert:\n      placeholder: sslcert 파일 경로\n      msg: sslcert 파일 경로는 비워둘 수 없습니다\n    ssl_key:\n      placeholder: sslkey 파일 경로\n      msg: sslkey 파일 경로는 비워둘 수 없습니다\n    config_yaml:\n      title: config.yaml 파일 생성\n      label: config.yaml 파일이 생성되었습니다.\n      desc: >-\n        config.yaml 파일을 <1>/var/wwww/xxx/</1> 디렉터리에 수동으로 생성하고 아래 텍스트를 붙여넣을 수 있습니다.\n      info: 위 작업을 완료한 후 \"다음\" 버튼을 클릭하세요.\n    site_information: 사이트 정보\n    admin_account: 관리자 계정\n    site_name:\n      label: 사이트 이름\n      msg: 사이트 이름을 입력하세요.\n      msg_max_length: 사이트 이름은 최대 30자여야 합니다.\n    site_url:\n      label: 사이트 URL\n      text: 사이트의 주소입니다.\n      msg:\n        empty: 사이트 URL을 입력하세요.\n        incorrect: 올바른 형식의 사이트 URL을 입력하세요.\n        max_length: 사이트 URL은 최대 512자여야 합니다.\n    contact_email:\n      label: 연락처 이메일\n      text: 이 사이트에 책임을 지는 주요 연락 이메일 주소입니다.\n      msg:\n        empty: 연락처 이메일을 입력하세요.\n        incorrect: 올바른 형식의 연락처 이메일을 입력하세요.\n    login_required:\n      label: 비공개\n      switch: 로그인 필요\n      text: 로그인한 사용자만 이 커뮤니티에 접근할 수 있습니다.\n    admin_name:\n      label: 이름\n      msg: 이름을 입력하세요.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: 이름은 2 자 이상 30 자 이하여야 합니다.\n    admin_password:\n      label: 비밀번호\n      text: >-\n        로그인에 필요한 비밀번호입니다. 안전한 위치에 보관하세요.\n      msg: 비밀번호를 입력하세요.\n      msg_min_length: 비밀번호는 최소 8자여야 합니다.\n      msg_max_length: 비밀번호는 최대 32자여야 합니다.\n    admin_confirm_password:\n      label: \"비밀번호 확인\"\n      text: \"확인을 위해 비밀번호를 다시 입력해주세요.\"\n      msg: \"비밀번호 확인이 일치하지 않습니다.\"\n    admin_email:\n      label: 이메일\n      text: 로그인에 필요한 이메일입니다.\n      msg:\n        empty: 이메일을 입력하세요.\n        incorrect: 올바른 형식의 이메일을 입력하세요.\n    ready_title: 귀하의 사이트가 준비되었습니다\n    ready_desc: >-\n      추가 설정을 원하시면 <1>관리자 섹션</1>에서 찾아보세요; 사이트 메뉴에서 확인할 수 있습니다.\n    good_luck: \"재미있고 행운을 빕니다!\"\n    warn_title: 경고\n    warn_desc: >-\n      파일 <1>config.yaml</1>이 이미 존재합니다. 이 파일의 구성 항목 중 재설정이 필요하면 먼저 삭제하세요.\n    install_now: <1>지금 설치해보세요</1>.\n    installed: 이미 설치됨\n    installed_desc: >-\n      이미 설치된 것으로 보입니다. 재설치하려면 먼저 이전 데이터베이스 테이블을 삭제하세요.\n    db_failed: 데이터베이스 연결 실패\n    db_failed_desc: >-\n      <1>config.yaml</1> 파일에 있는 데이터베이스 정보가 올바르지 않거나 데이터베이스 서버와 연결할 수 없습니다. 호스트의 데이터베이스 서버가 다운된 경우입니다.\n  counts:\n    views: 조회수\n    votes: 투표\n    answers: 답변\n    accepted: 채택됨\n  page_error:\n    http_error: HTTP 오류 {{ code }}\n    desc_403: 이 페이지에 접근할 권한이 없습니다.\n    desc_404: 죄송합니다. 이 페이지는 존재하지 않습니다.\n    desc_50X: 서버에서 오류가 발생하여 요청을 완료할 수 없습니다.\n    back_home: 홈페이지로 돌아가기\n  page_maintenance:\n    desc: \"저희는 현재 유지보수 중입니다. 곧 돌아오겠습니다.\"\n  nav_menus:\n    dashboard: 대시보드\n    contents: 콘텐츠\n    questions: 질문\n    answers: 답변\n    users: 사용자\n    badges: 뱃지\n    flags: 신고하기\n    settings: 설정\n    general: 일반\n    interface: 인터페이스\n    smtp: SMTP\n    branding: 브랜딩\n    legal: 법적 사항\n    write: 글 작성\n    terms: Terms\n    tos: 이용 약관\n    privacy: 개인정보 보호\n    seo: 검색 엔진 최적화\n    customize: 사용자 정의\n    themes: 테마\n    login: 로그인\n    privileges: 권한\n    plugins: 플러그인\n    installed_plugins: 설치된 플러그인\n    apperance: 모양\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: '{{site_name}}에 오신 것을 환영합니다'\n  user_center:\n    login: 로그인\n    qrcode_login_tip: '{{ agentName }}을(를) 사용하여 QR 코드를 스캔하고 로그인하세요.'\n    login_failed_email_tip: 로그인 실패, 다시 시도하기 전에 이 앱이 이메일 정보에 접근할 수 있도록 허용하세요.\n  badges:\n    modal:\n      title: 축하합니다\n      content: 새로운 뱃지를 획득했습니다.\n      close: 닫기\n      confirm: 뱃지 보기\n    title: 뱃지\n    awarded: 수여됨\n    earned_×: '{{ number }} 개 획득'\n    ×_awarded: \"{{ number }} 개 수여됨\"\n    can_earn_multiple: 이 뱃지는 여러 번 획득할 수 있습니다.\n    earned: 획득함\n  admin:\n    admin_header:\n      title: 관리자\n    dashboard:\n      title: 대시보드\n      welcome: 관리자에 오신 것을 환영합니다!\n      site_statistics: 사이트 통계\n      questions: \"질문:\"\n      resolved: \"해결됨:\"\n      unanswered: \"답변이 없는 질문:\"\n      answers: \"답변:\"\n      comments: \"댓글:\"\n      votes: \"투표:\"\n      users: \"사용자:\"\n      flags: \"신고:\"\n      reviews: \"리뷰:\"\n      site_health: 사이트 상태\n      version: \"버전:\"\n      https: \"HTTPS:\"\n      upload_folder: \"업로드 폴더:\"\n      run_mode: \"실행 모드:\"\n      private: 비공개\n      public: 공개\n      smtp: \"SMTP:\"\n      timezone: \"시간대:\"\n      system_info: 시스템 정보\n      go_version: \"Go 버전:\"\n      database: \"데이터베이스:\"\n      database_size: \"데이터베이스 크기:\"\n      storage_used: \"사용 중인 저장 공간:\"\n      uptime: \"가동 시간:\"\n      links: 링크\n      plugins: 플러그인\n      github: GitHub\n      blog: 블로그\n      contact: 연락처\n      forum: 포럼\n      documents: 문서\n      feedback: 피드백\n      support: 지원\n      review: 검토\n      config: 설정\n      update_to: 업데이트\n      latest: 최신 버전\n      check_failed: 확인 실패\n      \"yes\": \"예\"\n      \"no\": \"아니요\"\n      not_allowed: 허용되지 않음\n      allowed: 허용됨\n      enabled: 활성화됨\n      disabled: 비활성화됨\n      writable: 쓰기 가능\n      not_writable: 쓰기 불가능\n    flags:\n      title: 신고\n      pending: 처리 대기 중\n      completed: 완료됨\n      flagged: 신고됨\n      flagged_type: '{{ type }}로 신고됨'\n      created: 생성됨\n      action: 동작\n      review: 검토\n    user_role_modal:\n      title: 사용자 역할 변경\n      btn_cancel: 취소\n      btn_submit: 제출\n    new_password_modal:\n      title: 새 비밀번호 설정\n      form:\n        fields:\n          password:\n            label: 비밀번호\n            text: 사용자가 로그아웃되고 다시 로그인해야 합니다.\n            msg: 비밀번호는 8-32자여야 합니다.\n      btn_cancel: 취소\n      btn_submit: 제출\n    edit_profile_modal:\n      title: 프로필 수정\n      form:\n        fields:\n          display_name:\n            label: 표시 이름\n            msg_range: 표시 이름은 2-30 자 길이여야 합니다.\n          username:\n            label: 사용자 이름\n            msg_range: 유저 이름은 2-30 자 길이여야 합니다.\n          email:\n            label: 이메일\n            msg_invalid: 유효하지 않은 이메일 주소.\n      edit_success: 성공적으로 수정되었습니다\n      btn_cancel: 취소\n      btn_submit: 제출\n    user_modal:\n      title: 새 사용자 추가\n      form:\n        fields:\n          users:\n            label: 대량 사용자 추가\n            placeholder: \"홍길동, hong@example.com, BUSYopr2\\n김철수, kim@example.com, fpDntV8q\"\n            text: 쉼표로 구분하여 “이름, 이메일, 비밀번호”를 입력하세요. 한 줄에 한 명의 사용자.\n            msg: \"사용자의 이메일을 입력하세요. 한 줄에 한 명씩 입력하세요.\"\n          display_name:\n            label: 표시 이름\n            msg: 표시 이름은 2-30 자 길이여야 합니다.\n          email:\n            label: 이메일\n            msg: 이메일이 유효하지 않습니다.\n          password:\n            label: 비밀번호\n            msg: 비밀번호는 8-32자여야 합니다.\n      btn_cancel: 취소\n      btn_submit: 제출\n    users:\n      title: 사용자\n      name: 이름\n      email: 이메일\n      reputation: 평판\n      created_at: 생성 시간\n      delete_at: 삭제된 시간\n      suspend_at: 정지된 시간\n      suspend_until: 정지 기한\n      status: 상태\n      role: 역할\n      action: 동작\n      change: 변경\n      all: 전체\n      staff: 스탭\n      more: 더 보기\n      inactive: 비활성화됨\n      suspended: 정지됨\n      deleted: 삭제됨\n      normal: 일반\n      Moderator: 관리자\n      Admin: 관리자\n      User: 사용자\n      filter:\n        placeholder: \"이름 또는 사용자 ID로 필터링\"\n      set_new_password: 새 비밀번호 설정\n      edit_profile: 프로필 수정\n      change_status: 상태 변경\n      change_role: 역할 변경\n      show_logs: 로그 표시\n      add_user: 사용자 추가\n      deactivate_user:\n        title: 사용자 비활성화\n        content: 비활성화된 사용자는 이메일을 다시 확인해야 합니다.\n      delete_user:\n        title: 이 사용자 삭제\n        content: 정말로 이 사용자를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다!\n        remove: 사용자의 모든 질문, 답변, 댓글 등을 삭제합니다.\n        label: 사용자의 계정만 삭제하려면 이 옵션을 선택하지 마세요.\n        text: 사용자의 계정만 삭제하려면 이 항목을 선택하지 마십시오.\n      suspend_user:\n        title: 이 사용자 정지\n        content: 정지된 사용자는 로그인할 수 없습니다.\n        label: 사용자를 며칠 접근 금지 하시겠습니까?\n        forever: 무기한\n    questions:\n      page_title: 질문\n      unlisted: 비공개\n      post: 게시물\n      votes: 투표\n      answers: 답변\n      created: 생성됨\n      status: 상태\n      action: 동작\n      change: 변경\n      pending: 대기 중\n      filter:\n        placeholder: \"제목 또는 질문 ID로 필터링\"\n    answers:\n      page_title: 답변\n      post: 게시물\n      votes: 투표\n      created: 생성됨\n      status: 상태\n      action: 동작\n      change: 변경\n      filter:\n        placeholder: \"제목 또는 답변 ID로 필터링\"\n    general:\n      page_title: 일반\n      name:\n        label: 사이트 이름\n        msg: 사이트 이름을 입력하세요.\n        text: \"사이트 이름, 타이틀 태그에 사용됩니다.\"\n      site_url:\n        label: 사이트 URL\n        msg: 사이트 URL을 입력하세요.\n        validate: 유효한 URL을 입력하세요.\n        text: 사이트 주소입니다.\n      short_desc:\n        label: 짧은 사이트 설명\n        msg: 짧은 사이트 설명을 입력하세요.\n        text: \"홈페이지에서 사용되는 짧은 설명입니다.\"\n      desc:\n        label: 사이트 설명\n        msg: 사이트 설명을 입력하세요.\n        text: \"메타 설명 태그에 사용되는 한 문장 설명입니다.\"\n      contact_email:\n        label: 연락처 이메일\n        msg: 연락처 이메일을 입력하세요.\n        validate: 유효한 이메일 주소를 입력하세요.\n        text: 사이트를 책임지는 주요 연락처 이메일 주소입니다.\n      check_update:\n        label: 소프트웨어 업데이트\n        text: 소프트웨어 업데이트 자동 확인\n    interface:\n      page_title: 인터페이스\n      language:\n        label: 인터페이스 언어\n        msg: 인터페이스 언어를 선택하세요.\n        text: 페이지를 새로고침하면 언어가 변경됩니다.\n      time_zone:\n        label: 시간대\n        msg: 시간대를 선택하세요.\n        text: 본인과 같은 시간대의 도시를 선택하세요.\n      avatar:\n        label: 기본 아바타\n        text: 사용자 정의 아바타가 없는 사용자에게 표시됩니다.\n      gravatar_base_url:\n        label: Gravatar 기본 URL\n        text: Gravatar 공급자의 API 기본 URL입니다. 비어 있으면 무시됩니다.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: 발신 이메일\n        msg: 발신 이메일을 입력하세요.\n        text: 이메일 발신 주소입니다.\n      from_name:\n        label: 발신자 이름\n        msg: 발신자 이름을 입력하세요.\n        text: 이메일 발신 시 사용될 이름입니다.\n      smtp_host:\n        label: SMTP 호스트\n        msg: SMTP 호스트를 입력하세요.\n        text: 메일 서버 주소입니다.\n      encryption:\n        label: 암호화\n        msg: 암호화 방식을 선택하세요.\n        text: 대부분의 서버에서 SSL을 권장합니다.\n        ssl: SSL\n        tls: TLS\n        none: 없음\n      smtp_port:\n        label: SMTP 포트\n        msg: SMTP 포트는 1에서 65535 사이의 숫자여야 합니다.\n        text: 메일 서버의 포트 번호입니다.\n      smtp_username:\n        label: SMTP 사용자 이름\n        msg: SMTP 사용자 이름을 입력하세요.\n      smtp_password:\n        label: SMTP 비밀번호\n        msg: SMTP 비밀번호를 입력하세요.\n      test_email_recipient:\n        label: 테스트 이메일 수신자\n        text: 테스트 이메일을 받을 이메일 주소를 입력하세요.\n        msg: 테스트 이메일 수신자가 유효하지 않습니다.\n      smtp_authentication:\n        label: 인증 사용\n        title: SMTP 인증\n        msg: SMTP 인증을 선택하세요.\n        \"yes\": \"예\"\n        \"no\": \"아니오\"\n    branding:\n      page_title: 브랜딩\n      logo:\n        label: 로고\n        msg: 로고를 입력하세요.\n        text: 사이트 좌측 상단에 표시될 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 하며 가로 세로 비율은 3:1 이상이어야 합니다. 비워 둘 경우 사이트 제목 텍스트가 표시됩니다.\n      mobile_logo:\n        label: 모바일 로고\n        text: 사이트의 모바일 버전에서 사용할 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 합니다. 비워 둘 경우 \"로고\" 설정에서 이미지가 사용됩니다.\n      square_icon:\n        label: 정사각형 아이콘\n        msg: 정사각형 아이콘을 입력하세요.\n        text: 메타데이터 아이콘의 기본 이미지로 사용됩니다. 이상적으로는 512x512보다 큰 이미지여야 합니다.\n      favicon:\n        label: 파비콘\n        text: 사이트의 파비콘 이미지입니다. CDN에서 정상적으로 작동하려면 png 형식이어야 하며, 크기는 32x32로 조정됩니다. 비워 둘 경우 \"정사각형 아이콘\"이 사용됩니다.\n    legal:\n      page_title: 법적 고지\n      terms_of_service:\n        label: 서비스 이용 약관\n        text: \"여기에 서비스 이용 약관 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요.\"\n      privacy_policy:\n        label: 개인정보 보호 정책\n        text: \"여기에 개인정보 보호 정책 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요.\"\n      external_content_display:\n        label: 외부 콘텐츠\n        text: \"콘텐츠에는 외부 웹사이트에서 삽입된 이미지, 비디오 및 미디어가 포함됩니다.\"\n        always_display: 항상 외부 콘텐츠 표시\n        ask_before_display: 외부 콘텐츠 표시 전 확인\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: 답변 작성\n        label: 각 사용자는 각 질문에 대해 단 하나의 답변만 작성할 수 있습니다.\n        text: \"기존 답변을 개선하고 향상시키기 위해 편집 링크를 사용할 수 있습니다.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: 추천 태그\n        text: \"추천 태그가 기본적으로 드롭다운 목록에 표시됩니다.\"\n        msg:\n          contain_reserved: \"추천 태그에는 예약된 태그가 포함될 수 없습니다\"\n      required_tag:\n        title: 필수 태그 설정\n        label: '\"추천 태그\" 를 필수 태그로 설정'\n        text: \"모든 새로운 질문은 최소한 하나의 추천 태그가 있어야 합니다.\"\n      reserved_tags:\n        label: 예약된 태그\n        text: \"예약된 태그는 관리자만 사용할 수 있습니다.\"\n      image_size:\n        label: 최대 이미지 크기 (MB)\n        text: \"최대 이미지 업로드 크기입니다.\"\n      attachment_size:\n        label: 최대 첨부 파일 크기 (MB)\n        text: \"최대 첨부 파일 업로드 크기입니다.\"\n      image_megapixels:\n        label: 최대 이미지 메가픽셀\n        text: \"이미지에 허용되는 최대 메가픽셀 수입니다.\"\n      image_extensions:\n        label: 허용된 이미지 확장자\n        text: \"이미지 표시가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요.\"\n      attachment_extensions:\n        label: 인증된 첨부 파일 확장자\n        text: \"업로드가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요. 경고: 업로드를 허용하면 보안 문제가 발생할 수 있습니다.\"\n    seo:\n      page_title: 검색 엔진 최적화\n      permalink:\n        label: 영구 링크\n        text: 사용자 정의 URL 구조는 링크의 사용성과 미래 호환성을 향상시킬 수 있습니다.\n      robots:\n        label: robots.txt\n        text: 이 설정은 사이트 설정과 관련된 내용을 영구적으로 덮어씁니다.\n    themes:\n      page_title: 테마\n      themes:\n        label: 테마\n        text: 기존 테마를 선택하세요.\n      color_scheme:\n        label: 색상 스키마\n      navbar_style:\n        label: 네비바 배경 스타일\n      primary_color:\n        label: 주요 색상\n        text: 테마에서 사용할 색상을 수정합니다.\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS 및 HTML\n      custom_css:\n        label: 사용자 정의 CSS\n        text: >\n\n      head:\n        label: 헤드\n        text: >\n\n      header:\n        label: 헤더\n        text: >\n\n      footer:\n        label: 푸터\n        text: 본문의 바로 앞에 삽입됩니다.\n      sidebar:\n        label: 사이드바\n        text: 사이드바에 삽입됩니다.\n    login:\n      page_title: 로그인\n      membership:\n        title: 멤버십\n        label: 신규 등록 허용\n        text: 계정을 생성할 수 있는 사람을 제한하려면 끄세요.\n      email_registration:\n        title: 이메일 등록\n        label: 이메일 등록 허용\n        text: 이메일을 통한 새 계정 생성을 막으려면 끄세요.\n      allowed_email_domains:\n        title: 허용된 이메일 도메인\n        text: 사용자가 계정을 등록할 때 필수적으로 사용해야 하는 이메일 도메인입니다. 한 줄에 하나의 도메인을 입력하세요. 비어 있으면 무시됩니다.\n      private:\n        title: 비공개\n        label: 로그인 필수\n        text: 로그인한 사용자만이 이 커뮤니티에 접근할 수 있습니다.\n      password_login:\n        title: 비밀번호 로그인\n        label: 이메일과 비밀번호 로그인 허용\n        text: \"경고: 끄면 다른 로그인 방법을 설정하지 않았다면 로그인할 수 없을 수 있습니다.\"\n    installed_plugins:\n      title: 설치된 플러그인\n      plugin_link: 플러그인은 기능을 확장하고 확장합니다. <1> 플러그인 저장소</1>에서 플러그인을 찾을 수 있습니다.\n      filter:\n        all: 전체\n        active: 활성화됨\n        inactive: 비활성화됨\n        outdated: 오래된 상태\n      plugins:\n        label: 플러그인\n        text: 기존 플러그인을 선택하세요.\n      name: 이름\n      version: 버전\n      status: 상태\n      action: 작업\n      deactivate: 비활성화\n      activate: 활성화\n      settings: 설정\n    settings_users:\n      title: 사용자\n      avatar:\n        label: 기본 아바타\n        text: 사용자가 자신의 사용자 정의 아바타를 가지지 않았을 때 표시됩니다.\n      gravatar_base_url:\n        label: Gravatar 기본 URL\n        text: Gravatar 공급자의 API 기본 URL입니다. 비어 있으면 무시됩니다.\n      profile_editable:\n        title: 프로필 편집 가능\n      allow_update_display_name:\n        label: 사용자가 표시 이름을 변경할 수 있도록 허용\n      allow_update_username:\n        label: 사용자가 사용자 이름을 변경할 수 있도록 허용\n      allow_update_avatar:\n        label: 사용자가 프로필 이미지를 변경할 수 있도록 허용\n      allow_update_bio:\n        label: 사용자가 자기 소개를 변경할 수 있도록 허용\n      allow_update_website:\n        label: 사용자가 웹사이트를 변경할 수 있도록 허용\n      allow_update_location:\n        label: 사용자가 위치 정보를 변경할 수 있도록 허용\n    privilege:\n      title: 권한\n      level:\n        label: 권한에 필요한 평판 레벨\n        text: 권한에 필요한 평판 레벨을 선택하세요.\n      msg:\n        should_be_number: 입력값은 숫자여야 합니다.\n        number_larger_1: 숫자는 1 이상이어야 합니다.\n    badges:\n      action: 동작\n      active: 활성\n      activate: 활성화\n      all: 모두\n      awards: 수상\n      deactivate: 비활성화\n      filter:\n        placeholder: 이름, 배지:id 로 필터링\n      group: 그룹\n      inactive: 비활성\n      name: 이름\n      show_logs: 로그 표시\n      status: 상태\n      title: 뱃지\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (선택 사항)\n    empty: 비어 있을 수 없습니다\n    invalid: 유효하지 않습니다\n    btn_submit: 저장\n    not_found_props: \"필수 속성 {{ key }}을(를) 찾을 수 없습니다.\"\n    select: 선택\n  page_review:\n    review: 리뷰\n    proposed: 제안된\n    question_edit: 질문 편집\n    answer_edit: 답변 편집\n    tag_edit: 태그 편집\n    edit_summary: 편집 요약\n    edit_question: 질문 편집\n    edit_answer: 답변 편집\n    edit_tag: 태그 편집\n    empty: 남은 리뷰 작업이 없습니다.\n    approve_revision_tip: 이 리비전을 승인하시겠습니까?\n    approve_flag_tip: 이 신고를 승인하시겠습니까?\n    approve_post_tip: 이 게시물을 승인하시겠습니까?\n    approve_user_tip: 이 사용자를 승인하시겠습니까?\n    suggest_edits: 제안된 편집\n    flag_post: 게시물 신고\n    flag_user: 사용자 신고\n    queued_post: 대기 중인 게시물\n    queued_user: 대기 중인 사용자\n    filter_label: 유형\n    reputation: 평판\n    flag_post_type: 이 게시물을 {{ type }}로 신고 처리했습니다.\n    flag_user_type: 이 사용자를 {{ type }}로 신고 처리했습니다.\n    edit_post: 게시물 편집\n    list_post: 게시물 목록\n    unlist_post: 게시물 비공개\n  timeline:\n    undeleted: 복구됨\n    deleted: 삭제됨\n    downvote: 다운보트\n    upvote: 업보트\n    accept: 채택됨\n    cancelled: 취소됨\n    commented: 댓글 작성됨\n    rollback: 롤백\n    edited: 편집됨\n    answered: 답변됨\n    asked: 질문됨\n    closed: 닫힘\n    reopened: 다시 열림\n    created: 생성됨\n    pin: 고정됨\n    unpin: 고정 해제됨\n    show: 공개됨\n    hide: 비공개됨\n    title: \"다음을 위한 히스토리\"\n    tag_title: \"태그에 대한 타임라인\"\n    show_votes: \"투표 보기\"\n    n_or_a: 없음\n    title_for_question: \"질문에 대한 타임라인\"\n    title_for_answer: \"{{ author }}가 {{ title }}에 대한 답변에 대한 타임라인\"\n    title_for_tag: \"태그에 대한 타임라인\"\n    datetime: 날짜 및 시간\n    type: 유형\n    by: 작성자\n    comment: 코멘트\n    no_data: \"아무 데이터도 찾을 수 없습니다.\"\n  users:\n    title: 사용자\n    users_with_the_most_reputation: 이번 주 평판이 가장 높은 사용자들\n    users_with_the_most_vote: 이번 주 투표를 가장 많이 한 사용자들\n    staffs: 우리 커뮤니티 스태프\n    reputation: 평판\n    votes: 투표\n  prompt:\n    leave_page: 페이지를 떠나시겠습니까?\n    changes_not_save: 변경 사항이 저장되지 않을 수 있습니다.\n  draft:\n    discard_confirm: 초안을 삭제하시겠습니까?\n  messages:\n    post_deleted: 이 게시물은 삭제되었습니다.\n    post_cancel_deleted: 이 게시물이 삭제 취소되었습니다.\n    post_pin: 이 게시물이 고정되었습니다.\n    post_unpin: 이 게시물의 고정이 해제되었습니다.\n    post_hide_list: 이 게시물이 목록에서 숨겨졌습니다.\n    post_show_list: 이 게시물이 목록에 표시되었습니다.\n    post_reopen: 이 게시물이 다시 열렸습니다.\n    post_list: 이 게시물이 목록에 등록되었습니다.\n    post_unlist: 이 게시물이 목록에서 등록 해제되었습니다.\n    post_pending: 회원님의 게시물이 검토를 기다리고 있습니다. 미리보기입니다. 승인 후에 공개됩니다.\n    post_closed: 이 게시물이 닫혔습니다.\n    answer_deleted: 이 답변이 삭제되었습니다.\n    answer_cancel_deleted: 이 답변이 삭제 취소되었습니다.\n    change_user_role: 이 사용자의 역할이 변경되었습니다.\n    user_inactive: 이 사용자는 이미 비활성 상태입니다.\n    user_normal: 이 사용자는 이미 일반 사용자입니다.\n    user_suspended: 이 사용자가 정지되었습니다.\n    user_deleted: 이 사용자가 삭제되었습니다.\n    user_added: User has been added successfully.\n    badge_activated: 이 배지가 활성화되었습니다.\n    badge_inactivated: 이 배지가 비활성화되었습니다.\n    users_deleted: 이 사용자들이 삭제되었습니다.\n    posts_deleted: 이 질문들이 삭제되었습니다.\n    answers_deleted: 이 답변들이 삭제되었습니다.\n    copy: 클립보드에 복사\n    copied: 복사됨\n    external_content_warning: 외부 이미지/미디어가 표시되지 않습니다.\n\n\n"
  },
  {
    "path": "i18n/ml_IN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Edit\n    delete:\n      other: Delete\n    close:\n      other: Close\n    reopen:\n      other: Reopen\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: List\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Password\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: You cannot modify your password.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Edit Tag\n    ask_a_question: Create Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    new_alerts: New alerts\n    all_read: Mark all as read\n    show_more: Show more\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Invite People\n    desc: Invite people you think can answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for the same question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/nl_NL.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/no_NO.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/pl_PL.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Sukces.\n    unknown:\n      other: Nieznany błąd.\n    request_format_error:\n      other: Format żądania jest nieprawidłowy.\n    unauthorized_error:\n      other: Niezautoryzowany.\n    database_error:\n      other: Błąd serwera danych.\n    forbidden_error:\n      other: Zakazane.\n    duplicate_request_error:\n      other: Duplikat zgłoszenia.\n  action:\n    report:\n      other: Zgłoś\n    edit:\n      other: Edytuj\n    delete:\n      other: Usuń\n    close:\n      other: Zamknij\n    reopen:\n      other: Otwórz ponownie\n    forbidden_error:\n      other: Zakazane.\n    pin:\n      other: Przypnij\n    hide:\n      other: Usuń z listy\n    unpin:\n      other: Odepnij\n    show:\n      other: Lista\n    invite_someone_to_answer:\n      other: Edytuj\n    undelete:\n      other: Przywróć\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Użytkownik\n      admin:\n        other: Administrator\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Domyślnie bez specjalnego dostępu.\n      admin:\n        other: Posiadać pełne uprawnienia dostępu do strony.\n      moderator:\n        other: Ma dostęp do wszystkich postów z wyjątkiem ustawień administratora.\n  privilege:\n    level_1:\n      description:\n        other: Poziom 1 (mniejsza reputacja wymagana dla prywatnego zespołu, grupy)\n    level_2:\n      description:\n        other: Poziom 2 (niska reputacja wymagana dla społeczności startującej)\n    level_3:\n      description:\n        other: Poziom 3 (wysoka reputacja wymagana dla dojrzałej społeczności)\n    level_custom:\n      description:\n        other: Poziom niestandardowy\n    rank_question_add_label:\n      other: Zadaj pytanie\n    rank_answer_add_label:\n      other: Napisz odpowiedź\n    rank_comment_add_label:\n      other: Napisz komentarz\n    rank_report_add_label:\n      other: Zgłoś\n    rank_comment_vote_up_label:\n      other: Wyróżnij komentarz\n    rank_link_url_limit_label:\n      other: Opublikuj więcej niż 2 linki na raz\n    rank_question_vote_up_label:\n      other: Wyróżnij pytanie\n    rank_answer_vote_up_label:\n      other: Wyróżnij odpowiedź\n    rank_question_vote_down_label:\n      other: Oceń pytanie negatywnie\n    rank_answer_vote_down_label:\n      other: Oceń odpowiedź negatywnie\n    rank_invite_someone_to_answer_label:\n      other: Zaproś kogoś do odpowiedzi\n    rank_tag_add_label:\n      other: Utwórz nowy tag\n    rank_tag_edit_label:\n      other: Edytuj opis tagu (wymaga akceptacji)\n    rank_question_edit_label:\n      other: Edytuj pytanie innych (wymaga akceptacji)\n    rank_answer_edit_label:\n      other: Edytuj odpowiedź innych (wymaga akceptacji)\n    rank_question_edit_without_review_label:\n      other: Edytuj pytanie innych bez akceptacji\n    rank_answer_edit_without_review_label:\n      other: Edytuj odpowiedź innych bez akceptacji\n    rank_question_audit_label:\n      other: Przejrzyj edycje pytania\n    rank_answer_audit_label:\n      other: Przejrzyj edycje odpowiedzi\n    rank_tag_audit_label:\n      other: Przejrzyj edycje tagu\n    rank_tag_edit_without_review_label:\n      other: Edytuj opis tagu bez akceptacji\n    rank_tag_synonym_label:\n      other: Zarządzaj synonimami tagów\n  email:\n    other: E-mail\n  e_mail:\n    other: Email\n  password:\n    other: Hasło\n  pass:\n    other: Hasło\n  old_pass:\n    other: Current password\n  original_text:\n    other: Ten wpis\n  email_or_password_wrong_error:\n    other: Email lub hasło nie są poprawne.\n  error:\n    common:\n      invalid_url:\n        other: Nieprawidłowy URL.\n      status_invalid:\n        other: Nieprawidłowy status.\n    password:\n      space_invalid:\n        other: Hasło nie może zawierać spacji.\n    admin:\n      cannot_update_their_password:\n        other: Nie możesz zmieniać swojego hasła.\n      cannot_edit_their_profile:\n        other: Nie możesz modyfikować swojego profilu.\n      cannot_modify_self_status:\n        other: Nie możesz modyfikować swojego statusu.\n      email_or_password_wrong:\n        other: Emil lub hasło nie są zgodne.\n    answer:\n      not_found:\n        other: Odpowiedź nie została odnaleziona.\n      cannot_deleted:\n        other: Brak uprawnień do usunięcia.\n      cannot_update:\n        other: Brak uprawnień do aktualizacji.\n      question_closed_cannot_add:\n        other: Pytania są zamknięte i nie można ich dodawać.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Komentarz nie może edytować.\n      not_found:\n        other: Komentarz nie został odnaleziony.\n      cannot_edit_after_deadline:\n        other: Czas komentowania był zbyt długi, aby go zmodyfikować.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: E-mail już istnieje.\n      need_to_be_verified:\n        other: E-mail powinien zostać zweryfikowany.\n      verify_url_expired:\n        other: Adres URL zweryfikowanej wiadomości e-mail wygasł, prosimy o ponowne wysłanie wiadomości e-mail.\n      illegal_email_domain_error:\n        other: Wysyłanie wiadomości e-mail z tej domeny jest niedozwolone. Użyj innej domeny.\n    lang:\n      not_found:\n        other: Nie znaleziono pliku językowego.\n    object:\n      captcha_verification_failed:\n        other: Nieprawidłowa captcha.\n      disallow_follow:\n        other: Nie wolno ci podążać za nimi.\n      disallow_vote:\n        other: Nie masz uprawnień do głosowania.\n      disallow_vote_your_self:\n        other: Nie możesz głosować na własne posty.\n      not_found:\n        other: Obiekt nie został odnaleziony.\n      verification_failed:\n        other: Weryfikacja nie powiodła się.\n      email_or_password_incorrect:\n        other: Email lub hasło są nieprawidłowe.\n      old_password_verification_failed:\n        other: Stara weryfikacja hasła nie powiodła się\n      new_password_same_as_previous_setting:\n        other: Nowe hasło jest takie samo jak poprzednie.\n      already_deleted:\n        other: Ten wpis został usunięty.\n    meta:\n      object_not_found:\n        other: Meta obiekt nie został odnaleziony\n    question:\n      already_deleted:\n        other: Ten post został usunięty.\n      under_review:\n        other: Twój post oczekuje na recenzje. Będzie widoczny po jej akceptacji.\n      not_found:\n        other: Pytanie nie zostało odnalezione.\n      cannot_deleted:\n        other: Brak uprawnień do usunięcia.\n      cannot_close:\n        other: Brak uprawnień do zamknięcia.\n      cannot_update:\n        other: Brak uprawnień do edycji.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Ranga nie spełnia warunku.\n      vote_fail_to_meet_the_condition:\n        other: Dziękujemy za opinię. Potrzebujesz co najmniej {{.Rank}} reputacji, aby oddać głos.\n      no_enough_rank_to_operate:\n        other: Potrzebujesz co najmniej {{.Rank}} reputacji, aby to zrobić.\n    report:\n      handle_failed:\n        other: Nie udało się obsłużyć raportu.\n      not_found:\n        other: Raport nie został znaleziony.\n    tag:\n      already_exist:\n        other: Tag już istnieje.\n      not_found:\n        other: Tag nie został znaleziony.\n      recommend_tag_not_found:\n        other: Zalecany tag nie istnieje.\n      recommend_tag_enter:\n        other: Proszę wprowadzić przynajmniej jeden wymagany tag.\n      not_contain_synonym_tags:\n        other: Nie powinno zawierać tagów synonimów.\n      cannot_update:\n        other: Brak uprawnień do aktualizacji.\n      is_used_cannot_delete:\n        other: Nie możesz usunąć tagu, który jest w użyciu.\n      cannot_set_synonym_as_itself:\n        other: Nie można ustawić synonimu aktualnego tagu jako takiego.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Nazwą nadawcy nie może być adresem e-mail.\n    theme:\n      not_found:\n        other: Nie znaleziono motywu.\n    revision:\n      review_underway:\n        other: Nie można teraz edytować, istnieje wersja w kolejce sprawdzeń.\n      no_permission:\n        other: Brak uprawnień do wersji.\n    user:\n      external_login_missing_user_id:\n        other: Platforma zewnętrzna nie dostarcza unikalnego identyfikatora użytkownika (UserID), dlatego nie możesz się zalogować. Skontaktuj się z administratorem witryny.\n      external_login_unbinding_forbidden:\n        other: Proszę ustawić hasło logowania dla swojego konta przed usunięciem tego logowania.\n      email_or_password_wrong:\n        other:\n          other: Adres email i hasło nie są zgodne.\n      not_found:\n        other: Użytkownik nie został znaleziony.\n      suspended:\n        other: Użytkownik został zawieszony.\n      username_invalid:\n        other: Nazwa użytkownika jest nieprawidłowa.\n      username_duplicate:\n        other: Nazwa użytkownika jest już zajęta.\n      set_avatar:\n        other: Nie udało się ustawić awatara.\n      cannot_update_your_role:\n        other: Nie możesz zmienić swojej roli.\n      not_allowed_registration:\n        other: Obecnie strona nie zezwala na rejestracje.\n      not_allowed_login_via_password:\n        other: Obecnie strona nie zezwala na logowanie się za pomocą hasła.\n      access_denied:\n        other: Odmowa dostępu.\n      page_access_denied:\n        other: Nie masz dostępu do tej strony.\n      add_bulk_users_format_error:\n        other: \"Błąd {{.Field}} w pobliżu '{{.Content}}' w linii {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Liczba użytkowników, których dodasz na raz, powinna mieścić się w przedziale 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Nie udało się odczytać pliku konfiguracyjnego.\n    database:\n      connection_failed:\n        other: Nie udało się połączyć z bazą danych.\n      create_table_failed:\n        other: Nie udało się utworzyć tabeli.\n    install:\n      create_config_failed:\n        other: Nie można utworzyć pliku config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Nieobsługiwany format pliku.\n    site_info:\n      config_not_found:\n        other: Nie znaleziono konfiguracji strony.\n    badge:\n      object_not_found:\n        other: Nie znaleziono obiektu odznaki\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Ten post jest reklamą lub wandalizmem. Nie jest przydatny ani istotny dla bieżącego tematu.\n    rude_or_abusive:\n      name:\n        other: niegrzeczny lub obraźliwy\n      desc:\n        other: \"Rozsądna osoba uznałaby tę treść za nieodpowiednią do dyskusji opartej na szacunku.\"\n    a_duplicate:\n      name:\n        other: duplikat\n      desc:\n        other: To pytanie zostało już wcześniej zadane i ma już odpowiedź.\n      placeholder:\n        other: Wprowadź link do istniejącego pytania\n    not_a_answer:\n      name:\n        other: nie jest odpowiedzią\n      desc:\n        other: \"Ta wiadomość została zamieszczona jako odpowiedź, ale nie próbuje odpowiedzieć na pytanie. Powinna być prawdopodobnie edycją, komentarzem, kolejnym pytaniem lub całkowicie usunięta.\"\n    no_longer_needed:\n      name:\n        other: nie jest już potrzebne\n      desc:\n        other: Ten komentarz jest przestarzały, prowadzi do rozmowy lub nie jest związany z tą wiadomością.\n    something:\n      name:\n        other: coś innego\n      desc:\n        other: Ta wiadomość wymaga uwagi personelu z innego powodu, który nie jest wymieniony powyżej.\n      placeholder:\n        other: Poinformuj nas dokładnie, o co Ci chodzi\n    community_specific:\n      name:\n        other: powód specyficzny dla społeczności\n      desc:\n        other: To pytanie nie spełnia wytycznych społeczności.\n    not_clarity:\n      name:\n        other: wymaga szczegółów lub wyjaśnienia\n      desc:\n        other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno skupić się tylko na jednym problemie.\n    looks_ok:\n      name:\n        other: Wygląda poprawnie\n      desc:\n        other: Ta wiadomość jest dobra w obecnej formie i nie jest niskiej jakości.\n    needs_edit:\n      name:\n        other: wymaga edycji, a ja to zrobiłem/am\n      desc:\n        other: Popraw i skoryguj problemy w tej wiadomości samodzielnie.\n    needs_close:\n      name:\n        other: wymaga zamknięcia\n      desc:\n        other: Na zamknięte pytanie nie można odpowiadać, ale wciąż można edytować, głosować i komentować.\n    needs_delete:\n      name:\n        other: wymaga usunięcia\n      desc:\n        other: Ta wiadomość zostanie usunięta.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: To pytanie zostało już wcześniej zadane i ma już odpowiedź.\n      guideline:\n        name:\n          other: powód specyficzny dla społeczności\n        desc:\n          other: To pytanie nie spełnia wytycznych społeczności.\n      multiple:\n        name:\n          other: wymaga szczegółów lub wyjaśnienia\n        desc:\n          other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno się skupić tylko na jednym problemie.\n      other:\n        name:\n          other: coś innego\n        desc:\n          other: Ten post wymaga jeszcze jednego powodu, który nie został wymieniony powyżej.\n    operation_type:\n      asked:\n        other: zapytano\n      answered:\n        other: odpowiedziano\n      modified:\n        other: zmodyfikowane\n    deleted_title:\n      other: Usunięte pytanie\n    questions_title:\n      other: Pytania\n  tag:\n    tags_title:\n      other: Tagi\n    no_description:\n      other: Tag nie posiada opisu.\n  notification:\n    action:\n      update_question:\n        other: zaktualizował/a pytanie\n      answer_the_question:\n        other: odpowiedz na pytanie\n      update_answer:\n        other: aktualizuj odpowiedź\n      accept_answer:\n        other: zaakceptował/a odpowiedź\n      comment_question:\n        other: skomentował/a pytanie\n      comment_answer:\n        other: skomentował/a odpowiedź\n      reply_to_you:\n        other: odpowiedział/a tobie\n      mention_you:\n        other: wspomniał/a o tobie\n      your_question_is_closed:\n        other: Twoje pytanie zostało zamknięte\n      your_question_was_deleted:\n        other: Twoje pytanie zostało usunięte\n      your_answer_was_deleted:\n        other: Twoja odpowiedź została usunięta\n      your_comment_was_deleted:\n        other: Twój komentarz został usunięty\n      up_voted_question:\n        other: pytanie przegłosowane\n      down_voted_question:\n        other: pytanie odrzucone\n      up_voted_answer:\n        other: odpowiedź przegłosowana\n      down_voted_answer:\n        other: odrzucona odpowiedź\n      up_voted_comment:\n        other: komentarz upvote\n      invited_you_to_answer:\n        other: zaproszono Cię do odpowiedzi\n      earned_badge:\n        other: Zdobyłeś odznakę \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Potwierdź swój nowy adres e-mail\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} odpowiedział(-a) na pytanie\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} zaprosił(a) Cię do odpowiedzi\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} skomentował/-a Twój wpis\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nowe pytanie: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Reset hasła\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Potwierdź swoje nowe konto\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Wiadomość testowa\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: oceń pozytywnie\n    upvoted:\n      other: polubione\n    downvote:\n      other: oceń negatywnie\n    downvoted:\n      other: oceniono negatywnie\n    accept:\n      other: akceptuj\n    accepted:\n      other: zaakceptowane\n    edit:\n      other: edytuj\n  review:\n    queued_post:\n      other: Post w kolejce\n    flagged_post:\n      other: Post oznaczony\n    suggested_post_edit:\n      other: Sugerowane zmiany\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} już {{ .Count }} razy ...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiografista\n        desc:\n          other: Wypełniono informacje <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profil</a>.\n      certified:\n        name:\n          other: Certyfikowany\n        desc:\n          other: Ukończono nasz nowy samouczek.\n      editor:\n        name:\n          other: Edytor\n        desc:\n          other: Pierwsza edycja posta.\n      first_flag:\n        name:\n          other: Pierwsza flaga\n        desc:\n          other: Po raz pierwszy oznaczono post.\n      first_upvote:\n        name:\n          other: Pierwszy pozytywny głos\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: Pierwszy odnośnik\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: Pierwsza Reakcja\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: Pierwsze udostępnianie\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Zadane pytania i zaakceptowane odpowiedź.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Pozostaw 5 komentarzy.\n      new_user_of_the_month:\n        name:\n          other: Nowy użytkownik miesiąca\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Jak formatować\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Poprzedni\n    next: Następny\n  page_title:\n    question: Pytanie\n    questions: Pytania\n    tag: Tag\n    tags: Tagi\n    tag_wiki: wiki tagu\n    create_tag: Utwórz tag\n    edit_tag: Edytuj tag\n    ask_a_question: Create Question\n    edit_question: Edytuj pytanie\n    edit_answer: Edytuj odpowiedź\n    search: Szukaj\n    posts_containing: Posty zawierające\n    settings: Ustawienia\n    notifications: Powiadomienia\n    login: Zaloguj się\n    sign_up: Zarejestruj się\n    account_recovery: Odzyskiwanie konta\n    account_activation: Aktywacja konta\n    confirm_email: Potwierdź adres e-mail\n    account_suspended: Konto zawieszone\n    admin: Administrator\n    change_email: Zmień e-mail\n    install: Instalacja Answer\n    upgrade: Aktualizacja Answer\n    maintenance: Przerwa techniczna\n    users: Użytkownicy\n    oauth_callback: Przetwarzanie\n    http_404: Błąd HTTP 404\n    http_50X: Błąd HTTP 500\n    http_403: Błąd HTTP 403\n    logout: Wyloguj się\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Powiadomienia\n    inbox: Skrzynka odbiorcza\n    achievement: Osiągnięcia\n    new_alerts: Nowe powiadomienia\n    all_read: Oznacz wszystkie jako przeczytane\n    show_more: Pokaż więcej\n    someone: Ktoś\n    inbox_type:\n      all: Wszystko\n      posts: Posty\n      invites: Zaproszenia\n      votes: Głosy\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Twoje konto zostało zawieszone\n    until_time: \"Twoje konto zostało zawieszone do {{ time }}.\"\n    forever: Ten użytkownik został na zawsze zawieszony.\n    end: Nie spełniasz wytycznych społeczności.\n    contact_us: Skontaktuj się z nami\n  editor:\n    blockquote:\n      text: Cytat\n    bold:\n      text: Pogrubienie\n    chart:\n      text: Wykres\n      flow_chart: Wykres przepływu\n      sequence_diagram: Diagram sekwencji\n      class_diagram: Diagram klas\n      state_diagram: Diagram stanów\n      entity_relationship_diagram: Diagram związków encji\n      user_defined_diagram: Diagram zdefiniowany przez użytkownika\n      gantt_chart: Wykres Gantta\n      pie_chart: Wykres kołowy\n    code:\n      text: Przykład kodu\n      add_code: Dodaj przykład kodu\n      form:\n        fields:\n          code:\n            label: Kod\n            msg:\n              empty: Kod nie może być pusty.\n          language:\n            label: Język\n            placeholder: Wykrywanie automatyczne\n      btn_cancel: Anuluj\n      btn_confirm: Dodaj\n    formula:\n      text: Formuła\n      options:\n        inline: Formuła w linii\n        block: Formuła blokowa\n    heading:\n      text: Nagłówek\n      options:\n        h1: Nagłówek 1\n        h2: Nagłówek 2\n        h3: Nagłówek 3\n        h4: Nagłówek 4\n        h5: Nagłówek 5\n        h6: Nagłówek 6\n    help:\n      text: Pomoc\n    hr:\n      text: Linia pozioma\n    image:\n      text: Obrazek\n      add_image: Dodaj obrazek\n      tab_image: Prześlij obrazek\n      form_image:\n        fields:\n          file:\n            label: Plik graficzny\n            btn: Wybierz obrazek\n            msg:\n              empty: Plik nie może być pusty.\n              only_image: Dozwolone są tylko pliki obrazków.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Opis\n      tab_url: Adres URL obrazka\n      form_url:\n        fields:\n          url:\n            label: Adres URL obrazka\n            msg:\n              empty: Adres URL obrazka nie może być pusty.\n          name:\n            label: Opis\n      btn_cancel: Anuluj\n      btn_confirm: Dodaj\n      uploading: Przesyłanie\n    indent:\n      text: Wcięcie\n    outdent:\n      text: Wcięcie zewnętrzne\n    italic:\n      text: Podkreślenie\n    link:\n      text: Hiperłącze\n      add_link: Dodaj hiperłącze\n      form:\n        fields:\n          url:\n            label: Adres URL\n            msg:\n              empty: Adres URL nie może być pusty.\n          name:\n            label: Opis\n      btn_cancel: Anuluj\n      btn_confirm: Dodaj\n    ordered_list:\n      text: Lista numerowana\n    unordered_list:\n      text: Lista wypunktowana\n    table:\n      text: Tabela\n      heading: Nagłówek\n      cell: Komórka\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Zamykam ten post jako...\n    btn_cancel: Anuluj\n    btn_submit: Prześlij\n    remark:\n      empty: Nie może być puste.\n    msg:\n      empty: Proszę wybrać powód.\n  report_modal:\n    flag_title: Oznaczam ten post jako...\n    close_title: Zamykam ten post jako...\n    review_question_title: Przegląd pytania\n    review_answer_title: Przegląd odpowiedzi\n    review_comment_title: Przegląd komentarza\n    btn_cancel: Anuluj\n    btn_submit: Prześlij\n    remark:\n      empty: Nie może być puste.\n    msg:\n      empty: Proszę wybrać powód.\n      not_a_url: Format adresu URL jest nieprawidłowy.\n      url_not_match: Wskazany URL nie pasuje do bieżącej witryny.\n  tag_modal:\n    title: Utwórz nowy tag\n    form:\n      fields:\n        display_name:\n          label: Nazwa wyświetlana\n          msg:\n            empty: Nazwa wyświetlana nie może być pusta.\n            range: Nazwa wyświetlana może zawierać maksymalnie 35 znaków.\n        slug_name:\n          label: Adres URL slug\n          desc: Odnośnik URL może zawierać maksymalnie 35 znaków.\n          msg:\n            empty: Odnośnik URL nie może być pusty.\n            range: Odnośnik URL może zawierać maksymalnie 35 znaków.\n            character: Slug adresu URL zawiera niedozwolony zestaw znaków.\n        desc:\n          label: Opis\n        revision:\n          label: Wersja\n        edit_summary:\n          label: Podsumowanie edycji\n          placeholder: >-\n            Krótkie wyjaśnienie zmian (poprawa pisowni, naprawa gramatyki, poprawa formatowania)\n    btn_cancel: Anuluj\n    btn_submit: Prześlij\n    btn_post: Opublikuj nowy tag\n  tag_info:\n    created_at: Utworzony\n    edited_at: Edytowany\n    history: Historia\n    synonyms:\n      title: Synonimy\n      text: Następujące tagi zostaną przekierowane na\n      empty: Nie znaleziono synonimów.\n      btn_add: Dodaj synonim\n      btn_edit: Edytuj\n      btn_save: Zapisz\n    synonyms_text: Następujące tagi zostaną przekierowane na\n    delete:\n      title: Usuń ten tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Czy na pewno chcesz usunąć?\n      close: Zamknij\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edytuj tag\n    default_reason: Edytuj tag\n    default_first_reason: Dodaj tag\n    btn_save_edits: Zapisz edycje\n    btn_cancel: Anuluj\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [o] HH:mm\"\n    now: teraz\n    x_seconds_ago: \"{{count}} s temu\"\n    x_minutes_ago: \"{{count}} min temu\"\n    x_hours_ago: \"{{count}} h temu\"\n    hour: godzina\n    day: dzień\n    hours: godziny\n    days: dni\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: serce\n    smile: uśmiech\n    frown: niezadowolenie\n    btn_label: dodaj lub usuń reakcje\n    undo_emoji: cofnij reakcje {{ emoji }}\n    react_emoji: zareaguj {{ emoji }}\n    unreact_emoji: usuń reakcje {{ emoji }}\n  comment:\n    btn_add_comment: Dodaj komentarz\n    reply_to: Odpowiedź na\n    btn_reply: Odpowiedz\n    btn_edit: Edytuj\n    btn_delete: Usuń\n    btn_flag: Zgłoś\n    btn_save_edits: Zapisz edycje\n    btn_cancel: Anuluj\n    show_more: \"Jest {{count}} komentarzy, pokaż więcej\"\n    tip_question: >-\n      Użyj komentarzy, aby poprosić o dodatkowe informacje lub sugerować poprawki. Unikaj udzielania odpowiedzi na pytania w komentarzach.\n    tip_answer: >-\n      Użyj komentarzy, aby odpowiedzieć innym użytkownikom lub powiadomić ich o zmianach. Jeśli dodajesz nowe informacje, edytuj swój post zamiast komentować.\n    tip_vote: Dodaje coś wartościowego do posta\n  edit_answer:\n    title: Edytuj odpowiedź\n    default_reason: Edytuj odpowiedź\n    default_first_reason: Dodaj odpowiedź\n    form:\n      fields:\n        revision:\n          label: Rewizja\n        answer:\n          label: Odpowiedź\n          feedback:\n            characters: Treść musi mieć co najmniej 6 znaków.\n        edit_summary:\n          label: Podsumowanie edycji\n          placeholder: >-\n            Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania)\n    btn_save_edits: Zapisz edycje\n    btn_cancel: Anuluj\n  tags:\n    title: Tagi\n    sort_buttons:\n      popular: Popularne\n      name: Nazwa\n      newest: Najnowsze\n    button_follow: Obserwuj\n    button_following: Obserwowane\n    tag_label: pytania\n    search_placeholder: Filtruj według nazwy tagu\n    no_desc: Tag nie posiada opisu.\n    more: Więcej\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edytuj pytanie\n    default_reason: Edytuj pytanie\n    default_first_reason: Create question\n    similar_questions: Podobne pytania\n    form:\n      fields:\n        revision:\n          label: Rewizja\n        title:\n          label: Tytuł\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Tytuł nie może być pusty.\n            range: Tytuł do 150 znaków\n        body:\n          label: Treść\n          msg:\n            empty: Treść nie może być pusta.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tagi\n          msg:\n            empty: Tagi nie mogą być puste.\n        answer:\n          label: Odpowiedź\n          msg:\n            empty: Odpowiedź nie może być pusta.\n        edit_summary:\n          label: Podsumowanie edycji\n          placeholder: >-\n            Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania)\n    btn_post_question: Opublikuj swoje pytanie\n    btn_save_edits: Zapisz edycje\n    answer_question: Odpowiedz na swoje pytanie\n    post_question&answer: Opublikuj swoje pytanie i odpowiedź\n  tag_selector:\n    add_btn: Dodaj tag\n    create_btn: Utwórz nowy tag\n    search_tag: Wyszukaj tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Nie znaleziono pasujących tagów\n    tag_required_text: Wymagany tag (co najmniej jeden)\n  header:\n    nav:\n      question: Pytania\n      tag: Tagi\n      user: Użytkownicy\n      badges: Badges\n      profile: Profil\n      setting: Ustawienia\n      logout: Wyloguj\n      admin: Administrator\n      review: Recenzja\n      bookmark: Zakładki\n      moderation: Moderacja\n    search:\n      placeholder: Szukaj\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Zmień\n    loading: Wczytywanie...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Wpisz tekst z obrazka powyżej\n    msg:\n      empty: Captcha nie może być pusty.\n  inactive:\n    first: >-\n      Czas na ostatni krok! Wysłaliśmy wiadomość aktywacyjną na adres <bold>{{mail}}</bold>. Prosimy postępować zgodnie z instrukcjami zawartymi w wiadomości w celu aktywacji Twojego konta.\n    info: \"Jeśli nie dotarła, sprawdź folder ze spamem.\"\n    another: >-\n      Wysłaliśmy kolejną wiadomość aktywacyjną na adres <bold>{{mail}}</bold>. Może to potrwać kilka minut, zanim dotrze; upewnij się, że sprawdzasz folder ze spamem.\n    btn_name: Ponownie wyślij wiadomość aktywacyjną\n    change_btn_name: Zmień adres e-mail\n    msg:\n      empty: Nie może być puste.\n    resend_email:\n      url_label: Czy na pewno chcesz ponownie wysłać e-mail aktywacyjny?\n      url_text: Możesz również podać powyższy link aktywacyjny użytkownikowi.\n  login:\n    login_to_continue: Zaloguj się, aby kontynuować\n    info_sign: Nie masz jeszcze konta? <1>Zarejestruj się</1>\n    info_login: Masz już konto? <1>Zaloguj się</1>\n    agreements: Rejestrując się, wyrażasz zgodę na <1>politykę prywatności</1> i <3>warunki korzystania z usługi</3>.\n    forgot_pass: Zapomniałeś hasła?\n    name:\n      label: Imię\n      msg:\n        empty: Imię nie może być puste.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Adres e-mail\n      msg:\n        empty: Adres e-mail nie może być pusty.\n    password:\n      label: Hasło\n      msg:\n        empty: Hasło nie może być puste.\n        different: Wprowadzone hasła są niezgodne\n  account_forgot:\n    page_title: Zapomniałeś hasła\n    btn_name: Wyślij mi e-mail odzyskiwania\n    send_success: >-\n      Jeśli istnieje konto powiązane z adresem <strong>{{mail}}</strong>, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi resetowania hasła.\n    email:\n      label: Adres e-mail\n      msg:\n        empty: Adres e-mail nie może być pusty.\n  change_email:\n    btn_cancel: Anuluj\n    btn_update: Zaktualizuj adres e-mail\n    send_success: >-\n      Jeśli istnieje konto powiązane z adresem <strong>{{mail}}</strong>, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi zmiany adresu e-mail.\n    email:\n      label: Nowy email\n      msg:\n        empty: Adres e-mail nie może być pusty.\n  oauth:\n    connect: Połącz z {{ auth_name }}\n    remove: Usuń {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Dodaj e-mail odzyskiwania do swojego konta.\n    btn_update: Zaktualizuj adres e-mail\n    email:\n      label: Adres e-mail\n      msg:\n        empty: Adres e-mail nie może być pusty.\n    modal_title: Adres e-mail już istnieje.\n    modal_content: Ten adres e-mail jest już zarejestrowany. Czy na pewno chcesz połączyć się z istniejącym kontem?\n    modal_cancel: Zmień adres e-mail\n    modal_confirm: Połącz z istniejącym kontem\n  password_reset:\n    page_title: Resetowanie hasła\n    btn_name: Zresetuj moje hasło\n    reset_success: >-\n      Pomyślnie zmieniono hasło; zostaniesz przekierowany na stronę logowania.\n    link_invalid: >-\n      Przepraszamy, ten link do resetowania hasła jest już nieaktualny. Być może Twoje hasło jest już zresetowane?\n    to_login: Przejdź do strony logowania\n    password:\n      label: Hasło\n      msg:\n        empty: Hasło nie może być puste.\n        length: Długość musi wynosić od 8 do 32 znaków.\n        different: Wprowadzone hasła są niezgodne.\n    password_confirm:\n      label: Potwierdź nowe hasło\n  settings:\n    page_title: Ustawienia\n    goto_modify: Przejdź do modyfikacji\n    nav:\n      profile: Profil\n      notification: Powiadomienia\n      account: Konto\n      interface: Interfejs\n    profile:\n      heading: Profil\n      btn_name: Zapisz\n      display_name:\n        label: Nazwa wyświetlana\n        msg: Wyświetlana nazwa nie może być pusta.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Nazwa użytkownika\n        caption: Ludzie mogą oznaczać Cię jako \"@nazwa_użytkownika\".\n        msg: Nazwa użytkownika nie może być pusta.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Zdjęcie profilowe\n        gravatar: Gravatar\n        gravatar_text: Możesz zmienić obraz na stronie\n        custom: Własne\n        custom_text: Możesz przesłać własne zdjęcie.\n        default: Systemowe\n        msg: Prosimy o przesłanie awatara\n      bio:\n        label: O mnie\n      website:\n        label: Strona internetowa\n        placeholder: \"https://przyklad.com\"\n        msg: Nieprawidłowy format strony internetowej\n      location:\n        label: Lokalizacja\n        placeholder: \"Miasto, Kraj\"\n    notification:\n      heading: Powiadomienia email\n      turn_on: Włącz\n      inbox:\n        label: Powiadomienia skrzynki odbiorczej\n        description: Odpowiedzi na Twoje pytania, komentarze, zaproszenia i inne.\n      all_new_question:\n        label: Wszystkie nowe pytania\n        description: Otrzymuj powiadomienia o wszystkich nowych pytaniach. Do 50 pytań tygodniowo.\n      all_new_question_for_following_tags:\n        label: Wszystkie nowe pytania dla obserwowanych tagów\n        description: Otrzymuj powiadomienia o nowych pytaniach do obserwowanych tagów.\n    account:\n      heading: Konto\n      change_email_btn: Zmień adres e-mail\n      change_pass_btn: Zmień hasło\n      change_email_info: >-\n        Wysłaliśmy e-mail na ten adres. Prosimy postępować zgodnie z instrukcjami potwierdzającymi.\n      email:\n        label: Email\n      new_email:\n        label: Nowy Email\n        msg: Nowy Email nie może być pusty.\n      pass:\n        label: Aktualne hasło\n        msg: Hasło nie może być puste.\n      password_title: Hasło\n      current_pass:\n        label: Aktualne hasło\n        msg:\n          empty: Obecne hasło nie może być puste.\n          length: Długość musi wynosić od 8 do 32 znaków.\n          different: Dwa wprowadzone hasła nie są zgodne.\n      new_pass:\n        label: Nowe hasło\n      pass_confirm:\n        label: Potwierdź nowe hasło\n    interface:\n      heading: Interfejs\n      lang:\n        label: Język Interfejsu\n        text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony.\n    my_logins:\n      title: Moje logowania\n      label: Zaloguj się lub zarejestruj na tej stronie za pomocą tych kont.\n      modal_title: Usuń logowanie\n      modal_content: Czy na pewno chcesz usunąć to logowanie z Twojego konta?\n      modal_confirm_btn: Usuń\n      remove_success: Pomyślnie usunięto\n  toast:\n    update: pomyślnie zaktualizowane\n    update_password: Hasło zostało pomyślnie zmienione.\n    flag_success: Dzięki za zgłoszenie.\n    forbidden_operate_self: Zakazane działanie na sobie\n    review: Twoja poprawka zostanie wyświetlona po zatwierdzeniu.\n    sent_success: Wysyłanie zakończone powodzeniem\n  related_question:\n    title: Related\n    answers: odpowiedzi\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Ludzie pytali\n    desc: Wybierz osoby, które mogą znać odpowiedź.\n    invite: Zaproś do odpowiedzi\n    add: Dodaj osoby\n    search: Wyszukaj osoby\n  question_detail:\n    action: Akcja\n    created: Created\n    Asked: Zadane\n    asked: zadał(a)\n    update: Zmodyfikowane\n    Edited: Edited\n    edit: edytowany\n    commented: skomentowano\n    Views: Wyświetlone\n    Follow: Obserwuj\n    Following: Obserwuje\n    follow_tip: Obserwuj to pytanie, aby otrzymywać powiadomienia\n    answered: odpowiedziano\n    closed_in: Zamknięte za\n    show_exist: Pokaż istniejące pytanie.\n    useful: Przydatne\n    question_useful: Jest przydatne i jasne\n    question_un_useful: Jest niejasne lub nieprzydatne\n    question_bookmark: Dodaj do zakładek to pytanie\n    answer_useful: Jest przydatna\n    answer_un_useful: To nie jest użyteczne\n    answers:\n      title: Odpowiedzi\n      score: Ocena\n      newest: Najnowsze\n      oldest: Najstarsze\n      btn_accept: Akceptuj\n      btn_accepted: Zaakceptowane\n    write_answer:\n      title: Twoja odpowiedź\n      edit_answer: Edytuj moją obecną odpowiedź\n      btn_name: Wyślij swoją odpowiedź\n      add_another_answer: Dodaj kolejną odpowiedź\n      confirm_title: Kontynuuj odpowiedź\n      continue: Kontynuuj\n      confirm_info: >-\n        <p>Czy na pewno chcesz dodać kolejną odpowiedź? </p><p>Możesz zamiast tego użyć linku edycji, aby udoskonalić i poprawić istniejącą odpowiedź.</p>\n      empty: Odpowiedź nie może być pusta.\n      characters: Treść musi mieć co najmniej 6 znaków.\n      tips:\n        header_1: Dziękujemy za Twoją odpowiedź\n        li1_1: Prosimy, upewnij się, że <strong>odpowiadasz na pytanie</strong>. Podaj szczegóły i podziel się swoimi badaniami.\n        li1_2: Popieraj swoje stwierdzenia referencjami lub osobistym doświadczeniem.\n        header_2: Ale <strong>unikaj</strong> ...\n        li2_1: Prośby o pomoc, pytania o wyjaśnienie lub odpowiadanie na inne odpowiedzi.\n    reopen:\n      confirm_btn: Ponowne otwarcie\n      title: Otwórz ponownie ten post\n      content: Czy na pewno chcesz go ponownie otworzyć?\n    list:\n      confirm_btn: Lista\n      title: Pokaż ten post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Usuń z listy\n      title: Usuń ten post z listy\n      content: Czy na pewno chcesz usunąć z listy?\n    pin:\n      title: Przypnij ten post\n      content: Czy na pewno chcesz przypiąć go globalnie? Ten post będzie wyświetlany na górze wszystkich list postów.\n      confirm_btn: Przypnij\n  delete:\n    title: Usuń ten post\n    question: >-\n      Nie zalecamy <strong>usuwanie pytań wraz z udzielonymi</strong>, ponieważ pozbawia to przyszłych czytelników tej wiedzy.</p><p>Powtarzające się usuwanie pytań z odpowiedziami może skutkować zablokowaniem Twojego konta w zakresie zadawania pytań. Czy na pewno chcesz usunąć?\n    answer_accepted: >-\n      <p>Nie zalecamy <strong>usuwania zaakceptowanych już odpowiedzi</strong>, ponieważ pozbawia to przyszłych czytelników tej wiedzy. </p>Powtarzające się usuwanie zaakceptowanych odpowiedzi może skutkować zablokowaniem Twojego konta w zakresie udzielania odpowiedzi. Czy na pewno chcesz usunąć?\n    other: Czy na pewno chcesz usunąć?\n    tip_answer_deleted: Ta odpowiedź została usunięta\n    undelete_title: Cofnij usunięcie tego posta\n    undelete_desc: Czy na pewno chcesz cofnąć usunięcie?\n  btns:\n    confirm: Potwierdź\n    cancel: Anuluj\n    edit: Edytuj\n    save: Zapisz\n    delete: Usuń\n    undelete: Przywróć\n    list: Lista\n    unlist: Usuń z listy\n    unlisted: Usunięte z listy\n    login: Zaloguj się\n    signup: Zarejestruj się\n    logout: Wyloguj się\n    verify: Zweryfikuj\n    create: Create\n    approve: Zatwierdź\n    reject: Odrzuć\n    skip: Pominięcie\n    discard_draft: Odrzuć szkic\n    pinned: Przypięte\n    all: Wszystkie\n    question: Pytanie\n    answer: Odpowiedź\n    comment: Komentarz\n    refresh: Odśwież\n    resend: Wyślij ponownie\n    deactivate: Deaktywuj\n    active: Aktywne\n    suspend: Zawieś\n    unsuspend: Cofnij zawieszenie\n    close: Zamknij\n    reopen: Otwórz ponownie\n    ok: Ok\n    light: Jasny\n    dark: Ciemny\n    system_setting: Ustawienia systemowe\n    default: Domyślne\n    reset: Reset\n    tag: Tag\n    post_lowercase: wpis\n    filter: Filtry\n    ignore: Ignoruj\n    submit: Prześlij\n    normal: Normalny\n    closed: Zamknięty\n    deleted: Usunięty\n    deleted_permanently: Deleted permanently\n    pending: Oczekujący\n    more: Więcej\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Wyniki wyszukiwania\n    keywords: Słowa kluczowe\n    options: Opcje\n    follow: Obserwuj\n    following: Obserwuje\n    counts: \"Liczba wyników: {{count}}\"\n    counts_loading: \"... Results\"\n    more: Więcej\n    sort_btns:\n      relevance: Relewantność\n      newest: Najnowsze\n      active: Aktywne\n      score: Ocena\n      more: Więcej\n    tips:\n      title: Porady dotyczące wyszukiwania zaawansowanego\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> wyszukiwanie według autora\"\n      answer: \"<1> answers:0 </1> pytania bez odpowiedzi\"\n      score: \"<1>score:3</1> posty z oceną 3+\"\n      question: \"<1>is:question</1> wyszukiwanie pytań\"\n      is_answer: \"<1>is:answer</1> wyszukiwanie odpowiedzi\"\n    empty: Nie mogliśmy niczego znaleźć. <br />Wypróbuj inne lub mniej konkretne słowa kluczowe.\n  share:\n    name: Udostępnij\n    copy: Skopiuj link\n    via: Udostępnij post za pośrednictwem...\n    copied: Skopiowano\n    facebook: Udostępnij na Facebooku\n    twitter: Share to X\n  cannot_vote_for_self: Nie możesz głosować na własne posty.\n  modal_confirm:\n    title: Błąd...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Twoje nowe konto zostało potwierdzone; zostaniesz przekierowany na stronę główną.\n    link: Kontynuuj do strony głównej\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Twój adres e-mail został zaktualizowany.\n    confirm_new_email_invalid: >-\n      Przepraszamy, ten link potwierdzający jest już nieaktualny. Być może twój adres e-mail został już zmieniony?\n  unsubscribe:\n    page_title: Wypisz się\n    success_title: Pomyślne anulowanie subskrypcji\n    success_desc: Zostałeś pomyślnie usunięty z listy subskrybentów i nie będziesz otrzymywać dalszych wiadomości e-mail od nas.\n    link: Zmień ustawienia\n  question:\n    following_tags: Obserwowane tagi\n    edit: Edytuj\n    save: Zapisz\n    follow_tag_tip: Obserwuj tagi, aby dostosować listę pytań.\n    hot_questions: Gorące pytania\n    all_questions: Wszystkie pytania\n    x_questions: \"{{ count }} pytań\"\n    x_answers: \"{{ count }} odpowiedzi\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Pytania\n    answers: Odpowiedzi\n    newest: Najnowsze\n    active: Aktywne\n    hot: Gorące\n    frequent: Frequent\n    recommend: Polecane\n    score: Ocena\n    unanswered: Bez odpowiedzi\n    modified: zmodyfikowane\n    answered: udzielone odpowiedzi\n    asked: zadane\n    closed: zamknięte\n    follow_a_tag: Podążaj za tagiem\n    more: Więcej\n  personal:\n    overview: Przegląd\n    answers: Odpowiedzi\n    answer: odpowiedź\n    questions: Pytania\n    question: pytanie\n    bookmarks: Zakładki\n    reputation: Reputacja\n    comments: Komentarze\n    votes: Głosy\n    badges: Odznaczenia\n    newest: Najnowsze\n    score: Ocena\n    edit_profile: Edytuj Profil\n    visited_x_days: \"Odwiedzone przez {{ count }} dni\"\n    viewed: Wyświetlone\n    joined: Dołączył\n    comma: \",\"\n    last_login: Widziano\n    about_me: O mnie\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Najlepsze odpowiedzi\n    top_questions: Najlepsze pytania\n    stats: Statystyki\n    list_empty: Nie znaleziono wpisów. <br />Być może chcesz wybrać inną kartę?\n    content_empty: No posts found.\n    accepted: Zaakceptowane\n    answered: Udzielone odpowiedzi\n    asked: zapytano\n    downvoted: oceniono negatywnie\n    mod_short: MOD\n    mod_long: Moderatorzy\n    x_reputation: reputacja\n    x_votes: otrzymane głosy\n    x_answers: odpowiedzi\n    x_questions: pytania\n    recent_badges: Recent Badges\n  install:\n    title: Instalacja\n    next: Dalej\n    done: Zakończono\n    config_yaml_error: Nie można utworzyć pliku config.yaml.\n    lang:\n      label: Wybierz język\n    db_type:\n      label: Silnik bazy danych\n    db_username:\n      label: Nazwa użytkownika\n      placeholder: root\n      msg: Nazwa użytkownika nie może być pusta.\n    db_password:\n      label: Hasło\n      placeholder: turbo-tajne-hasło\n      msg: Hasło nie może być puste.\n    db_host:\n      label: Host bazy danych (ewentualnie dodatkowo port)\n      placeholder: \"db.domena:3306\"\n      msg: Host bazy danych nie może być pusty.\n    db_name:\n      label: Nazwa bazy danych\n      placeholder: odpowiedź\n      msg: Nazwa bazy danych nie może być pusta.\n    db_file:\n      label: Plik bazy danych\n      placeholder: /data/answer.db\n      msg: Plik bazy danych nie może być pusty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Utwórz plik config.yaml\n      label: Plik config.yaml utworzony.\n      desc: >-\n        Możesz ręcznie utworzyć plik <1>config.yaml</1> w katalogu <1>/var/wwww/xxx/</1> i wkleić do niego poniższy tekst.\n      info: Gdy już to zrobisz, kliknij przycisk \"Dalej\".\n    site_information: Informacje o witrynie\n    admin_account: Konto administratora\n    site_name:\n      label: Nazwa witryny\n      msg: Nazwa witryny nie może być pusta.\n      msg_max_length: Nazwa witryny musi mieć maksymalnie 30 znaków.\n    site_url:\n      label: Adres URL\n      text: Adres twojej strony.\n      msg:\n        empty: Adres URL nie może być pusty.\n        incorrect: Niepoprawny format adresu URL.\n        max_length: Adres URL witryny musi mieć maksymalnie 512 znaków.\n    contact_email:\n      label: Email kontaktowy\n      text: Email do osób odpowiedzialnych za tą witrynę.\n      msg:\n        empty: Email kontaktowy nie może być pusty.\n        incorrect: Email do kontaktu ma niepoprawny format.\n    login_required:\n      label: Prywatne\n      switch: Wymagane logowanie\n      text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy.\n    admin_name:\n      label: Imię\n      msg: Imię nie może być puste.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Hasło\n      text: >-\n        Będziesz potrzebować tego hasła do logowania. Przechowuj je w bezpiecznym miejscu.\n      msg: Hasło nie może być puste.\n      msg_min_length: Hasło musi mieć co najmniej 8 znaków.\n      msg_max_length: Hasło musi mieć maksymalnie 32 znaki.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: Będziesz potrzebować tego adresu e-mail do logowania.\n      msg:\n        empty: Adres e-mail nie może być pusty.\n        incorrect: Niepoprawny format adresu e-mail.\n    ready_title: Twoja strona jest gotowa\n    ready_desc: >-\n      Jeśli kiedykolwiek zechcesz zmienić więcej ustawień, odwiedź <1>sekcję administratora</1>; znajdziesz ją w menu strony.\n    good_luck: \"Baw się dobrze i powodzenia!\"\n    warn_title: Ostrzeżenie\n    warn_desc: >-\n      Plik <1>config.yaml</1> już istnieje. Jeśli chcesz zresetować którekolwiek z elementów konfiguracji w tym pliku, proszę go najpierw usunąć.\n    install_now: Możesz teraz <1>rozpocząć instalację</1>.\n    installed: Już zainstalowane\n    installed_desc: >-\n      Wygląda na to, że masz już zainstalowaną instancję Answer. Aby zainstalować ponownie, proszę najpierw wyczyścić tabele bazy danych.\n    db_failed: Połączenie z bazą danych nie powiodło się\n    db_failed_desc: >-\n      Oznacza to, że informacje o bazie danych w pliku <1>config.yaml</1> są nieprawidłowe lub że nie można nawiązać połączenia z serwerem bazy danych. Może to oznaczać, że serwer bazy danych wskazanego hosta nie działa.\n  counts:\n    views: widoki\n    votes: głosów\n    answers: odpowiedzi\n    accepted: Zaakceptowane\n  page_error:\n    http_error: Błąd HTTP {{ code }}\n    desc_403: Nie masz uprawnień do dostępu do tej strony.\n    desc_404: Niestety, ta strona nie istnieje.\n    desc_50X: Serwer napotkał błąd i nie mógł zrealizować twojego żądania.\n    back_home: Powrót do strony głównej\n  page_maintenance:\n    desc: \"Trwa konserwacja, wrócimy wkrótce.\"\n  nav_menus:\n    dashboard: Panel kontrolny\n    contents: Zawartość\n    questions: Pytania\n    answers: Odpowiedzi\n    users: Użytkownicy\n    badges: Badges\n    flags: Flagi\n    settings: Ustawienia\n    general: Ogólne\n    interface: Interfejs\n    smtp: SMTP\n    branding: Marka\n    legal: Prawne\n    write: Pisanie\n    terms: Terms\n    tos: Warunki korzystania z usługi\n    privacy: Prywatność\n    seo: SEO\n    customize: Dostosowywanie\n    themes: Motywy\n    login: Logowanie\n    privileges: Uprawnienia\n    plugins: Wtyczki\n    installed_plugins: Zainstalowane wtyczki\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Witamy w serwisie {{site_name}}\n  user_center:\n    login: Zaloguj się\n    qrcode_login_tip: Zeskanuj kod QR za pomocą {{ agentName }} i zaloguj się.\n    login_failed_email_tip: Logowanie nie powiodło się, przed ponowną próbą zezwól na dostęp tej aplikacji do informacji o Twojej skrzynce pocztowej.\n  badges:\n    modal:\n      title: Gratulacje\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Administrator\n    dashboard:\n      title: Panel\n      welcome: Witaj Administratorze!\n      site_statistics: Statystyki witryny\n      questions: \"Pytania:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Odpowiedzi:\"\n      comments: \"Komentarze:\"\n      votes: \"Głosy:\"\n      users: \"Użytkownicy:\"\n      flags: \"Flagi:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Wersja:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Prześlij folder:\"\n      run_mode: \"Tryb pracy:\"\n      private: Prywatne\n      public: Publiczne\n      smtp: \"SMTP:\"\n      timezone: \"Strefa czasowa:\"\n      system_info: Informacje o systemie\n      go_version: \"Wersja Go:\"\n      database: \"Baza danych:\"\n      database_size: \"Wielkość bazy danych:\"\n      storage_used: \"Wykorzystane miejsce:\"\n      uptime: \"Czas pracy:\"\n      links: Linki\n      plugins: Wtyczki\n      github: GitHub\n      blog: Blog\n      contact: Kontakt\n      forum: Forum\n      documents: Dokumenty\n      feedback: Opinie\n      support: Wsparcie\n      review: Przegląd\n      config: Konfiguracja\n      update_to: Zaktualizuj do\n      latest: Najnowszej\n      check_failed: Sprawdzanie nie powiodło się\n      \"yes\": \"Tak\"\n      \"no\": \"Nie\"\n      not_allowed: Nie dozwolone\n      allowed: Dozwolone\n      enabled: Włączone\n      disabled: Wyłączone\n      writable: Zapis i odczyt\n      not_writable: Nie można zapisać\n    flags:\n      title: Flagi\n      pending: Oczekujące\n      completed: Zakończone\n      flagged: Oznaczone\n      flagged_type: Oznaczone {{ type }}\n      created: Utworzone\n      action: Akcja\n      review: Przegląd\n    user_role_modal:\n      title: Zmień rolę użytkownika na...\n      btn_cancel: Anuluj\n      btn_submit: Wyślij\n    new_password_modal:\n      title: Ustaw nowe hasło\n      form:\n        fields:\n          password:\n            label: Hasło\n            text: Użytkownik zostanie wylogowany i musi zalogować się ponownie.\n            msg: Hasło musi mieć od 8 do 32 znaków.\n      btn_cancel: Anuluj\n      btn_submit: Prześlij\n    edit_profile_modal:\n      title: Edytuj profil\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Nazwa\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Błędny adresy email.\n      edit_success: Edycja zakończona pomyślnie\n      btn_cancel: Anuluj\n      btn_submit: Prześlij\n    user_modal:\n      title: Dodaj nowego użytkownika\n      form:\n        fields:\n          users:\n            label: Masowo dodaj użytkownika\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Oddziel \"nazwa, e-mail, hasło\" przecinkami. Jeden użytkownik na linię.\n            msg: \"Podaj adresy e-mail użytkowników, jeden w każdej linii.\"\n          display_name:\n            label: Nazwa wyświetlana\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: E-mail\n            msg: Email nie jest prawidłowy.\n          password:\n            label: Hasło\n            msg: Hasło musi mieć od 8 do 32 znaków.\n      btn_cancel: Anuluj\n      btn_submit: Prześlij\n    users:\n      title: Użytkownicy\n      name: Imię\n      email: E-mail\n      reputation: Reputacja\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Rola\n      action: Akcja\n      change: Zmień\n      all: Wszyscy\n      staff: Personel\n      more: Więcej\n      inactive: Nieaktywni\n      suspended: Zawieszeni\n      deleted: Usunięci\n      normal: Normalni\n      Moderator: Moderator\n      Admin: Administrator\n      User: Użytkownik\n      filter:\n        placeholder: \"Filtruj według imienia, użytkownik:id\"\n      set_new_password: Ustaw nowe hasło\n      edit_profile: Edytuj profil\n      change_status: Zmień status\n      change_role: Zmień rolę\n      show_logs: Pokaż logi\n      add_user: Dodaj użytkownika\n      deactivate_user:\n        title: Dezaktywuj użytkownika\n        content: Nieaktywny użytkownik musi ponownie potwierdzić swój adres email.\n      delete_user:\n        title: Usuń tego użytkownika\n        content: Czy na pewno chcesz usunąć tego użytkownika? Ta operacja jest nieodwracalna!\n        remove: Usuń zawartość\n        label: Usuń wszystkie pytania, odpowiedzi, komentarze itp.\n        text: Nie zaznaczaj tego, jeśli chcesz usunąć tylko konto użytkownika.\n      suspend_user:\n        title: Zawieś tego użytkownika\n        content: Zawieszony użytkownik nie może się logować.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Pytania\n      unlisted: Unlisted\n      post: Wpis\n      votes: Głosy\n      answers: Odpowiedzi\n      created: Utworzone\n      status: Status\n      action: Akcja\n      change: Zmień\n      pending: Oczekuje\n      filter:\n        placeholder: \"Filtruj według tytułu, pytanie:id\"\n    answers:\n      page_title: Odpowiedzi\n      post: Wpis\n      votes: Głosy\n      created: Utworzone\n      status: Status\n      action: Akcja\n      change: Zmień\n      filter:\n        placeholder: \"Filtruj według tytułu, odpowiedź:id\"\n    general:\n      page_title: Ogólne\n      name:\n        label: Nazwa witryny\n        msg: Nazwa strony nie może być pusta.\n        text: \"Nazwa tej strony, używana w tagu tytułu.\"\n      site_url:\n        label: URL strony\n        msg: Adres Url witryny nie może być pusty.\n        validate: Podaj poprawny adres URL.\n        text: Adres Twojej strony.\n      short_desc:\n        label: Krótki opis witryny\n        msg: Krótki opis strony nie może być pusty.\n        text: \"Krótki opis, używany w tagu tytułu na stronie głównej.\"\n      desc:\n        label: Opis witryny\n        msg: Opis strony nie może być pusty.\n        text: \"Opisz tę witrynę w jednym zdaniu, użytym w znaczniku meta description.\"\n      contact_email:\n        label: Email kontaktowy\n        msg: Email kontaktowy nie może być pusty.\n        validate: Email kontaktowy nie jest poprawny.\n        text: Adres email głównego kontaktu odpowiedzialnego za tę stronę.\n      check_update:\n        label: Aktualizacjia oprogramowania\n        text: Automatycznie sprawdzaj dostępność aktualizacji\n    interface:\n      page_title: Interfejs\n      language:\n        label: Język Interfejsu\n        msg: Język interfejsu nie może być pusty.\n        text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony.\n      time_zone:\n        label: Strefa czasowa\n        msg: Strefa czasowa nie może być pusta.\n        text: Wybierz miasto w tej samej strefie czasowej, co Ty.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Email nadawcy\n        msg: Email nadawcy nie może być pusty.\n        text: Adres email, z którego są wysyłane wiadomości.\n      from_name:\n        label: Nazwa w polu Od\n        msg: Nazwa nadawcy nie może być pusta.\n        text: Nazwa, z której są wysyłane wiadomości.\n      smtp_host:\n        label: Serwer SMTP\n        msg: Host SMTP nie może być pusty.\n        text: Twój serwer poczty.\n      encryption:\n        label: Szyfrowanie\n        msg: Szyfrowanie nie może być puste.\n        text: Dla większości serwerów zalecana jest opcja SSL.\n        ssl: SSL\n        tls: TLS\n        none: Brak\n      smtp_port:\n        label: Port SMTP\n        msg: Port SMTP musi być liczbą od 1 do 65535.\n        text: Port Twojego serwera poczty.\n      smtp_username:\n        label: Nazwa użytkownika SMTP\n        msg: Nazwa użytkownika SMTP nie może być pusta.\n      smtp_password:\n        label: Hasło SMTP\n        msg: Hasło SMTP nie może być puste.\n      test_email_recipient:\n        label: Odbiorcy testowych wiadomości email\n        text: Podaj adres email, który otrzyma testowe wiadomości.\n        msg: Odbiorcy testowych wiadomości email są nieprawidłowi\n      smtp_authentication:\n        label: Wymagaj uwierzytelniania\n        title: Uwierzytelnianie SMTP\n        msg: Uwierzytelnianie SMTP nie może być puste.\n        \"yes\": \"Tak\"\n        \"no\": \"Nie\"\n    branding:\n      page_title: Marka\n      logo:\n        label: Logo\n        msg: Logo nie może być puste.\n        text: Obrazek logo znajdujący się na górze lewej strony Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli i proporcjach większych niż 3:1. Jeśli zostanie puste, wyświetlony zostanie tekst tytułu strony.\n      mobile_logo:\n        label: Logo mobilne\n        text: Logo używane w wersji mobilnej Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli. Jeśli zostanie puste, użyty zostanie obrazek z ustawienia \"logo\".\n      square_icon:\n        label: Kwadratowa ikona\n        msg: Kwadratowa ikona nie może być pusta.\n        text: Obrazek używany jako podstawa dla ikon metadanych. Powinien mieć idealnie większe wymiary niż 512x512 pikseli.\n      favicon:\n        label: Ikona (favicon)\n        text: Ulubiona ikona witryny. Aby działać poprawnie przez CDN, musi to być png. Rozmiar zostanie zmieniony na 32x32. Jeśli pozostanie puste, zostanie użyta \"kwadratowa ikona\".\n    legal:\n      page_title: Prawne\n      terms_of_service:\n        label: Warunki korzystania z usługi\n        text: \"Możesz tutaj dodać treść regulaminu. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL.\"\n      privacy_policy:\n        label: Polityka prywatności\n        text: \"Możesz tutaj dodać treść polityki prywatności. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Każdy użytkownik może napisać tylko jedną odpowiedź na każde pytanie\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Rekomendowane tagi\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Każde nowe pytanie musi mieć przynajmniej jeden rekomendowany tag.\"\n      reserved_tags:\n        label: Zarezerwowane tagi\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Link bezpośredni\n        text: Dostosowane struktury URL mogą poprawić użyteczność i kompatybilność w przyszłości Twoich linków.\n      robots:\n        label: robots.txt\n        text: To trwale zastąpi wszelkie związane z witryną ustawienia.\n    themes:\n      page_title: Motywy\n      themes:\n        label: Motywy\n        text: Wybierz istniejący motyw.\n      color_scheme:\n        label: Schemat kolorów\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Kolor podstawowy\n        text: Zmodyfikuj kolory używane przez Twoje motywy.\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS i HTML\n      custom_css:\n        label: Własny CSS\n        text: >\n\n      head:\n        label: Głowa\n        text: >\n\n      header:\n        label: Nagłówek\n        text: >\n\n      footer:\n        label: Stopka\n        text: Zostanie wstawione przed &lt;/body>.\n      sidebar:\n        label: Pasek boczny\n        text: Będzie wstawiony w pasku bocznym.\n    login:\n      page_title: Logowanie\n      membership:\n        title: Członkostwo\n        label: Zezwalaj na nowe rejestracje\n        text: Wyłącz, aby uniemożliwić komukolwiek tworzenie nowego konta.\n      email_registration:\n        title: Rejestracja przez email\n        label: Zezwalaj na rejestrację przez email\n        text: Wyłącz, aby uniemożliwić tworzenie nowego konta poprzez email.\n      allowed_email_domains:\n        title: Dozwolone domeny email\n        text: Domeny email, z których użytkownicy muszą rejestrować konta. Jeden domena na linię. Ignorowane, gdy puste.\n      private:\n        title: Prywatne\n        label: Wymagane logowanie\n        text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy.\n      password_login:\n        title: Hasło logowania\n        label: Zezwalaj na logowanie email i hasłem\n        text: \"OSTRZEŻENIE: Jeśli wyłączone, już się nie zalogujesz, jeśli wcześniej nie skonfigurowałeś innej metody logowania.\"\n    installed_plugins:\n      title: Zainstalowane wtyczki\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: Wszystkie\n        active: Aktywne\n        inactive: Nieaktywne\n        outdated: Przestarzałe\n      plugins:\n        label: Wtyczki\n        text: Wybierz istniejącą wtyczkę.\n      name: Nazwa\n      version: Wersja\n      status: Status\n      action: Akcja\n      deactivate: Deaktywuj\n      activate: Aktywuj\n      settings: Ustawienia\n    settings_users:\n      title: Użytkownicy\n      avatar:\n        label: Domyślny Awatar\n        text: Dla użytkowników bez własnego awatara.\n      gravatar_base_url:\n        label: Adres URL bazy Gravatar\n        text: Adres URL bazy API dostawcy Gravatar. Ignorowane, gdy puste.\n      profile_editable:\n        title: Edycja profilu\n      allow_update_display_name:\n        label: Zezwalaj użytkownikom na zmianę wyświetlanej nazwy\n      allow_update_username:\n        label: Zezwalaj użytkownikom na zmianę nazwy użytkownika\n      allow_update_avatar:\n        label: Zezwalaj użytkownikom na zmianę obrazka profilowego\n      allow_update_bio:\n        label: Zezwalaj użytkownikom na zmianę opisu\n      allow_update_website:\n        label: Zezwalaj użytkownikom na zmianę witryny\n      allow_update_location:\n        label: Zezwalaj użytkownikom na zmianę lokalizacji\n    privilege:\n      title: Uprawnienia\n      level:\n        label: Wymagany poziom reputacji\n        text: Wybierz reputację wymaganą dla uprawnień\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Grupa\n      inactive: Inactive\n      name: Name\n      show_logs: Wyświetl dzienniki\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (opcjonalne)\n    empty: nie może być puste\n    invalid: jest nieprawidłowe\n    btn_submit: Zapisz\n    not_found_props: \"Nie znaleziono wymaganej właściwości {{ key }}.\"\n    select: Wybierz\n  page_review:\n    review: Przegląd\n    proposed: zaproponowany\n    question_edit: Edycja pytania\n    answer_edit: Edycja odpowiedzi\n    tag_edit: Edycja tagu\n    edit_summary: Podsumowanie edycji\n    edit_question: Edytuj pytanie\n    edit_answer: Edytuj odpowiedź\n    edit_tag: Edytuj tag\n    empty: Nie ma więcej zadań do przeglądu.\n    approve_revision_tip: Czy akceptujesz tę wersję?\n    approve_flag_tip: Czy akceptujesz tę flagę?\n    approve_post_tip: Czy zatwierdzasz ten post?\n    approve_user_tip: Czy zatwierdzasz tego użytkownika?\n    suggest_edits: Suggested edits\n    flag_post: Oznacz wpis\n    flag_user: Oznacz użytkownika\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Typ\n    reputation: reputacja\n    flag_post_type: Oznaczono ten wpis jako {{ type }}.\n    flag_user_type: Oznaczono tego użytkownika jako {{ type }}.\n    edit_post: Edytuj wpis\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: nieusunięte\n    deleted: usunięty\n    downvote: odrzucenie\n    upvote: głos za\n    accept: akceptacja\n    cancelled: anulowane\n    commented: skomentowano\n    rollback: wycofaj\n    edited: edytowany\n    answered: odpowiedziano\n    asked: zapytano\n    closed: zamknięty\n    reopened: ponownie otwarty\n    created: utworzony\n    pin: przypięty\n    unpin: odpięty\n    show: wymieniony\n    hide: niewidoczne\n    title: \"Historia dla\"\n    tag_title: \"Historia dla\"\n    show_votes: \"Pokaż głosy\"\n    n_or_a: Nie dotyczy\n    title_for_question: \"Historia dla\"\n    title_for_answer: \"Oś czasu dla odpowiedzi na {{ tytuł }} autorstwa {{ autor }}\"\n    title_for_tag: \"Oś czasu dla tagu\"\n    datetime: Data i czas\n    type: Typ\n    by: Przez\n    comment: Komentarz\n    no_data: \"Nie mogliśmy nic znaleźć.\"\n  users:\n    title: Użytkownicy\n    users_with_the_most_reputation: Użytkownicy o najwyższej reputacji w tym tygodniu\n    users_with_the_most_vote: Użytkownicy, którzy oddali najwięcej głosów w tym tygodniu\n    staffs: Nasz personel społeczności\n    reputation: reputacja\n    votes: głosy\n  prompt:\n    leave_page: Czy na pewno chcesz opuścić stronę?\n    changes_not_save: Twoje zmiany mogą nie zostać zapisane.\n  draft:\n    discard_confirm: Czy na pewno chcesz odrzucić swoją wersję roboczą?\n  messages:\n    post_deleted: Ten post został usunięty.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: Ten post został przypięty.\n    post_unpin: Ten post został odpięty.\n    post_hide_list: Ten post został ukryty na liście.\n    post_show_list: Ten post został wyświetlony na liście.\n    post_reopen: Ten post został ponownie otwarty.\n    post_list: Ten wpis został umieszczony na liście.\n    post_unlist: Ten wpis został usunięty z listy.\n    post_pending: Twój wpis oczekuje na recenzje. Będzie widoczny po jej akceptacji.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/pt_BR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Sucesso.\n    unknown:\n      other: Erro desconhecido.\n    request_format_error:\n      other: O formato da Requisição não é válido.\n    unauthorized_error:\n      other: Não autorizado.\n    database_error:\n      other: Erro no servidor de dados.\n  role:\n    name:\n      user:\n        other: Usuário\n      admin:\n        other: Administrador\n      moderator:\n        other: Moderador\n    description:\n      user:\n        other: Padrão sem acesso especial.\n      admin:\n        other: Possui controle total para acessar o site.\n      moderator:\n        other: Não possui controle a todas as postagens exceto configurações de administrador.\n  email:\n    other: E-mail\n  password:\n    other: Senha\n  email_or_password_wrong_error:\n    other: E-mail e Senha inválidos.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: E-mail e Senha inválidos.\n    answer:\n      not_found:\n        other: Resposta não encontrada.\n      cannot_deleted:\n        other: Permissão insuficiente para deletar.\n      cannot_update:\n        other: Permissão insuficiente para atualizar.\n    comment:\n      edit_without_permission:\n        other: Não é permitido atualizar comentários.\n      not_found:\n        other: Comentário não encontrado.\n      cannot_edit_after_deadline:\n        other: O tempo para comentários expirou.\n    email:\n      duplicate:\n        other: E-mail já existe na base de dados.\n      need_to_be_verified:\n        other: E-mail precisa ser verificado.\n      verify_url_expired:\n        other: A URL de validação do e-mail expirou, por favor, re-envie o e-mail.\n    lang:\n      not_found:\n        other: Arquivo de idioma não encontrado.\n    object:\n      captcha_verification_failed:\n        other: Captcha inválido.\n      disallow_follow:\n        other: Você não possui permissão para seguir.\n      disallow_vote:\n        other: Você não possui permissão para votar.\n      disallow_vote_your_self:\n        other: Você não pode votar na sua própria postagem.\n      not_found:\n        other: Objeto não encontrado.\n      verification_failed:\n        other: Falha na verificação.\n      email_or_password_incorrect:\n        other: E-mail e Senha não conferem.\n      old_password_verification_failed:\n        other: Verficação de senhas antigas falhou.\n      new_password_same_as_previous_setting:\n        other: A nova senha é idêntica à senha anterior.\n    question:\n      not_found:\n        other: Pergunta não encontrada.\n      cannot_deleted:\n        other: Não possui permissão para deletar.\n      cannot_close:\n        other: Não possui permissão para fechar.\n      cannot_update:\n        other: Não possui permissão para atualizar..\n    rank:\n      fail_to_meet_the_condition:\n        other: A classificação não atende à condição.\n    report:\n      handle_failed:\n        other: Falha no processamento do relatório.\n      not_found:\n        other: Relatório não encontrado.\n    tag:\n      not_found:\n        other: Marcador não encontada.\n      recommend_tag_not_found:\n        other: Marcador recomendado não existe.\n      recommend_tag_enter:\n        other: Por favor, insira ao menos um marcador obrigatório.\n      not_contain_synonym_tags:\n        other: Não pode contar marcadores de sinônimos.\n      cannot_update:\n        other: Não possui permissão para atualizar.\n      cannot_set_synonym_as_itself:\n        other: Você não pode definir o sinônimo do marcador atual como ela mesma.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: O De Nome não pode ser um endereço de e-mail.\n    Tema:\n      not_found:\n        other: Tema não encontrado.\n    revision:\n      review_underway:\n        other: Não é possível editar no momento, há uma versão na fila de revisão.\n      no_permission:\n        other: Sem pemissão para revisar.\n    user:\n      email_or_password_wrong:\n        other:\n          other: E-mail e senha não correspondem.\n      not_found:\n        other: Usuário não encontrado.\n      suspended:\n        other: Usuário foi suspenso.\n      username_invalid:\n        other: Nome de usuário inválido.\n      username_duplicate:\n        other: Nome de usuário já está em uso.\n      set_avatar:\n        other: Falha ao configurar Avatar.\n      cannot_update_your_role:\n        other: Você não pode modificar a sua função.\n      not_allowed_registration:\n        other: Atualmente o site não está aberto para cadastro\n    config:\n      read_config_failed:\n        other: A leitura da configuração falhou\n    database:\n      connection_failed:\n        other: Falha na conexão com o banco de dados\n      create_table_failed:\n        other: Falha na criação de tabela\n    install:\n      create_config_failed:\n        other: Não foi possível criar o arquivo config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Formato de arquivo não suportado.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Este post é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual.\n    rude:\n      name:\n        other: rude ou abusivo\n      desc:\n        other: Uma pessoa razoável acharia este conteúdo impróprio para um discurso respeitoso.\n    duplicate:\n      name:\n        other: uma duplicidade\n      desc:\n        other: Esta pergunta já foi feita antes e já tem uma resposta.\n    not_answer:\n      name:\n        other: não é uma resposta\n      desc:\n        other: Isso foi postado como uma resposta, mas não tenta responder à pergunta. Possivelmente deve ser uma edição, um comentário, outra pergunta ou excluído completamente.\n    not_need:\n      name:\n        other: não é mais necessário\n      desc:\n        other: Este comentário está desatualizado, coloquial ou não é relevante para esta postagem.\n    other:\n      name:\n        other: algo mais\n      desc:\n        other: Esta postagem requer atenção da equipe por outro motivo não listado acima.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Esta pergunta já foi feita antes e já tem uma resposta.\n      guideline:\n        name:\n          other: um motivo específico da comunidade\n        desc:\n          other: Esta pergunta não atende a uma diretriz da comunidade.\n      multiple:\n        name:\n          other: precisa de detalhes ou clareza\n        desc:\n          other: Esta pergunta atualmente inclui várias perguntas em uma. Deve se concentrar em apenas um problema.\n      other:\n        name:\n          other: algo mais\n        desc:\n          other: Este post requer outro motivo não listado acima.\n    operation_type:\n      asked:\n        other: perguntado\n      answered:\n        other: respondido\n      modified:\n        other: modificado\n  notification:\n    action:\n      update_question:\n        other: pergunta atualizada\n      answer_the_question:\n        other: pergunta respondida\n      update_answer:\n        other: resposta atualizada\n      accept_answer:\n        other: resposta aceita\n      comment_question:\n        other: pergunta comentada\n      comment_answer:\n        other: resposta comentada\n      reply_to_you:\n        other: respondeu a você\n      mention_you:\n        other: mencionou você\n      your_question_is_closed:\n        other: A sua pergunta foi fechada\n      your_question_was_deleted:\n        other: A sua pergunta foi deletada\n      your_answer_was_deleted:\n        other: A sua resposta foi deletada\n      your_comment_was_deleted:\n        other: O seu comentário foi deletado\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Como formatar\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">para mais links</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Título](https://url.com)</code></pre></li><li><p class=\"mb-2\">colocar retornos entre parágrafos</p></li><li><p class=\"mb-2\"><em>_italic_</em> ou **<strong>bold</strong>**</p></li><li><p class=\"mb-2\">identar código por 4 espaços</p></li><li><p class=\"mb-2\">cotação colocando <code>&gt;</code> no início da linha</p></li><li><p class=\"mb-2\">aspas invertidas <code>`tipo_isso`</code></p></li><li><p class=\"mb-2\">criar cercas de código com aspas invertidas <code>`</code></p><pre class=\"mb-0\"><code>```<br/>código aqui<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Anterior\n    next: Próximo\n  page_title:\n    question: Pergunta\n    questions: Perguntas\n    tag: Marcador\n    tags: Marcadores\n    tag_wiki: marcador wiki\n    edit_tag: Editar Marcador\n    ask_a_question: Adicionar Pergunta\n    edit_question: Editar Pergunta\n    edit_answer: Editar Resposta\n    search: Busca\n    posts_containing: Postagens contendo\n    settings: Configurações\n    notifications: Notificações\n    login: Entrar\n    sign_up: Criar conta\n    account_recovery: Recuperação de conta\n    account_activation: Ativação de conta\n    confirm_email: Confirmação de e-mail\n    account_suspended: Conta suspensa\n    admin: Administrador\n    change_email: Modificar e-mail\n    install: Instalação do Resposta\n    upgrade: Atualização do Resposta\n    maintenance: Manutenção do Website\n    users: Usuários\n  notifications:\n    title: Notificações\n    inbox: Caixa de entrada\n    achievement: Conquistas\n    all_read: Marcar todas como lidas\n    show_more: Mostrar mais\n  suspended:\n    title: A sua conta foi suspensa\n    until_time: \"A sua conta foi suspensa até {{ time }}.\"\n    forever: Este usuário foi suspenso por tempo indeterminado.\n    end: Você não atende a uma diretriz da comunidade.\n  editor:\n    blockquote:\n      text: Bloco de citação\n    bold:\n      text: Forte\n    chart:\n      text: Gráfico\n      flow_chart: Fluxograma\n      sequence_diagram: Diagrama de sequência\n      class_diagram: Diagrama de classe\n      state_diagram: Diagrama de estado\n      entity_relationship_diagram: Diagrama de relacionamento de entidade\n      user_defined_diagram: Diagrama definido pelo usuário\n      gantt_chart: Diagrama de Gantt\n      pie_chart: Gráfico de pizza\n    code:\n      text: Código de exemplo\n      add_code: Adicione código de exemplo\n      form:\n        fields:\n          code:\n            label: Código\n            msg:\n              empty: Código não pode ser vazio.\n          language:\n            label: Idioma (opcional)\n            placeholder: Detecção automática\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n    formula:\n      text: Fórmula\n      options:\n        inline: Fórmula em linha\n        block: Fórmula em bloco\n    Cabeçalho:\n      text: Cabeçalho\n      options:\n        h1: Cabeçalho 1\n        h2: Cabeçalho 2\n        h3: Cabeçalho 3\n        h4: Cabeçalho 4\n        h5: Cabeçalho 5\n        h6: Cabeçalho 6\n    help:\n      text: Ajuda\n    hr:\n      text: Régua horizontal\n    image:\n      text: Imagem\n      add_image: Adicionar imagem\n      tab_image: Enviar imagem\n      form_image:\n        fields:\n          file:\n            label: Arquivo de imagem\n            btn: Selecione imagem\n            msg:\n              empty: Arquivo não pode ser vazio.\n              only_image: Somente um arquivo de imagem é permitido.\n              max_size: O tamanho do arquivo não pode exceder 4 MB.\n          desc:\n            label: Descrição (opcional)\n      tab_url: URL da imagem\n      form_url:\n        fields:\n          url:\n            label: URL da imagem\n            msg:\n              empty: URL da imagem não pode ser vazia.\n          name:\n            label: Descrição (opcional)\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n      uploading: Enviando\n    indent:\n      text: Identação\n    outdent:\n      text: Não identado\n    italic:\n      text: Ênfase\n    link:\n      text: Superlink (Hyperlink)\n      add_link: Adicionar superlink (hyperlink)\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL não pode ser vazia.\n          name:\n            label: Descrição (opcional)\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n    ordered_list:\n      text: Lista numerada\n    unordered_list:\n      text: Lista com marcadores\n    table:\n      text: Tabela\n      Cabeçalho: Cabeçalho\n      cell: Célula\n  close_modal:\n    title: Estou fechando este post como...\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: Não pode ser vazio.\n    msg:\n      empty: Por favor selecione um motivo.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: Estou fechando este post como...\n    review_question_title: Revisar pergunta\n    review_answer_title: Revisar resposta\n    review_comment_title: Revisar comentário\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: Não pode ser vazio.\n    msg:\n      empty: Por favor selecione um motivo.\n  tag_modal:\n    title: Criar novo marcador\n    form:\n      fields:\n        display_name:\n          label: Nome de exibição\n          msg:\n            empty: Nome de exibição não pode ser vazio.\n            range: Nome de exibição tem que ter até 35 caracteres.\n        slug_name:\n          label: Slug de URL\n          desc: 'Deve usar o conjunto de caracteres \"a-z\", \"0-9\", \"+ # - .\"'\n          msg:\n            empty: URL slug não pode ser vazio.\n            range: URL slug até 35 caracteres.\n            character: URL slug contém conjunto de caracteres não permitido.\n        desc:\n          label: Descrição (opcional)\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n  tag_info:\n    created_at: Criado\n    edited_at: Editado\n    history: Histórico\n    synonyms:\n      title: Sinônimos\n      text: Os marcadores a seguir serão re-mapeados para\n      empty: Sinônimos não encotrados.\n      btn_add: Adicionar um sinônimo\n      btn_edit: Editar\n      btn_save: Salvar\n    synonyms_text: Os marcadores a seguir serão re-mapeados para\n    delete:\n      title: Deletar este marcador\n      content: >-\n        <p>Não permitimos a exclusão de marcadores com postagens.</p><p>Por favor, remova este marcador das postagens primeiro.</p>\n      content2: Você tem certeza que deseja deletar?\n      close: Fechar\n  edit_tag:\n    title: Editar marcador\n    default_reason: Editar marcador\n    form:\n      fields:\n        revision:\n          label: Revisão\n        display_name:\n          label: Nome de exibição\n        slug_name:\n          label: Slug de URL\n          info: 'Deve usar o conjunto de caracteres \"a-z\", \"0-9\", \"+ # - .\"'\n        desc:\n          label: Descrição\n        edit_summary:\n          label: Resumo da edição\n          placeholder: >-\n            Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n  dates:\n    long_date: D MMM\n    long_date_with_year: \"D MMM, YYYY\"\n    long_date_with_time: \"D MMM, YYYY [at] HH:mm\"\n    now: agora\n    x_seconds_ago: \"{{count}}s atrás\"\n    x_minutes_ago: \"{{count}}m atrás\"\n    x_hours_ago: \"{{count}}h atrás\"\n    hour: hora\n    day: dia\n  comment:\n    btn_add_comment: Adicionar comentário\n    reply_to: Responder a\n    btn_reply: Responder\n    btn_edit: Editar\n    btn_delete: Deletar\n    btn_flag: Marcador\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n    show_more: Mostrar mais comentários\n    tip_question: >-\n      Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários.\n    tip_answer: >-\n      Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar.\n  edit_answer:\n    title: Editar Resposta\n    default_reason: Editar Resposta\n    form:\n      fields:\n        revision:\n          label: Revisão\n        answer:\n          label: Resposta\n          feedback:\n            caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento.\n        edit_summary:\n          label: Resumo da edição\n          placeholder: >-\n            Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n  tags:\n    title: Marcadores\n    sort_buttons:\n      popular: Popular\n      name: Nome\n      newest: mais recente\n    button_follow: Seguir\n    button_following: Seguindo\n    tag_label: perguntas\n    search_placeholder: Filtrar por nome de marcador\n    no_desc: O marcador não possui descrição.\n    more: Mais\n  ask:\n    title: Adicionar Pergunta\n    edit_title: Editar Pergunta\n    default_reason: Editar pergunta\n    similar_questions: Perguntas similares\n    form:\n      fields:\n        revision:\n          label: Revisão\n        title:\n          label: Título\n          placeholder: Seja específico e tenha em mente que está fazendo uma pergunta a outra pessoa\n          msg:\n            empty: Título não pode ser vazio.\n            range: Título até 150 caracteres\n        body:\n          label: Corpo\n          msg:\n            empty: Corpo da mensagem não pode ser vazio.\n        tags:\n          label: Marcadores\n          msg:\n            empty: Marcadores não podes ser vazios.\n        answer:\n          label: Resposta\n          msg:\n            empty: Resposta não pode ser vazia.\n        edit_summary:\n          label: Resumo da edição\n          placeholder: >-\n            Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_post_question: Publicar a sua pergunta\n    btn_save_edits: Salvar edições\n    answer_question: Responda a sua própria pergunta\n    post_question&answer: Publicar a sua pergunta e resposta\n  tag_selector:\n    add_btn: Adicionar marcador\n    create_btn: Criar novo marcador\n    search_tag: Procurar marcador\n    hint: \"Descreva do quê se trata a sua pergunta, ao menos um marcador é requirido.\"\n    no_result: Nenhum marcador correspondente\n    tag_required_text: Marcador obrigatório (ao menos um)\n  header:\n    nav:\n      question: Perguntas\n      tag: Marcadores\n      user: Usuários\n      profile: Perfil\n      setting: Configurações\n      logout: Sair\n      admin: Administrador\n      review: Revisar\n    search:\n      placeholder: Procurar\n  footer:\n    build_on: >-\n      Desenvolvido com base no <1> Answer </1> — o software de código aberto que alimenta comunidades de perguntas e respostas.<br />Feito com amor © {{cc}}.\n  upload_img:\n    name: Mudar\n    loading: carregando...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Escreva o texto acima\n    msg:\n      empty: Captcha não pode ser vazio.\n  inactive:\n    first: >-\n      Você está quase pronto! Enviamos um e-mail de ativação para <bold>{{mail}}</bold>. Por favor, siga as instruções no e-mail para ativar uma conta sua.\n    info: \"Se não chegar, verifique sua pasta de spam.\"\n    another: >-\n      Enviamos outro e-mail de ativação para você em <bold>{{mail}}</bold>. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam.\n    btn_name: Reenviar e-mail de ativação\n    change_btn_name: Mudar email\n    msg:\n      empty: Não pode ser vazio.\n  login:\n    page_title: Bem vindo ao {{site_name}}\n    login_to_continue: Entre para continuar\n    info_sign: Não possui uma conta? <1>Cadastrar-se</1>\n    info_login: Já possui uma conta? <1>Entre</1>\n    agreements: Ao se registrar, você concorda com as <1>políticas de privacidades</1> e os <3>termos de serviços</3>.\n    forgot_pass: Esqueceu a sua senha?\n    name:\n      label: Nome\n      msg:\n        empty: Nome não pode ser vazio.\n        range: Nome até 30 caracteres.\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n    password:\n      label: Senha\n      msg:\n        empty: Senha não pode ser vazia.\n        different: As senhas inseridas em ambos os campos são inconsistentes\n  account_forgot:\n    page_title: Esqueceu a sua senha\n    btn_name: Enviar e-mail de recuperação de senha\n    send_success: >-\n      Se uma conta corresponder <strong>{{mail}}</strong>, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve.\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n  change_email:\n    page_title: Bem vindo ao {{site_name}}\n    btn_cancel: Cancelar\n    btn_update: Atualiza email address\n    send_success: >-\n      Se uma conta corresponder <strong>{{mail}}</strong>, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve.\n    email:\n      label: Novo E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n  password_reset:\n    page_title: Redefinir senha\n    btn_name: Redefinir minha senha\n    reset_success: >-\n      Você alterou com sucesso uma senha sua; você será redirecionado para a página de login.\n    link_invalid: >-\n      Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida?\n    to_login: Continuar para a tela de login\n    password:\n      label: Senha\n      msg:\n        empty: Senha não pode ser vazio.\n        length: O comprimento deve estar entre 8 e 32\n        different: As senhas inseridas em ambos os campos são inconsistentes\n    password_confirm:\n      label: Confirmar Nova senha\n  settings:\n    page_title: Configurações\n    nav:\n      profile: Perfil\n      notification: Notificações\n      account: Conta\n      interface: Interface\n    profile:\n      Cabeçalho: Perfil\n      btn_name: Salvar\n      display_name:\n        label: Nome de exibição\n        msg: Nome de exibição não pode ser vazio.\n        msg_range: Nome de exibição tem que ter até 30 caracteres.\n      username:\n        label: Nome de usuário\n        caption: As pessoas poderão mensionar você com \"@usuário\".\n        msg: Nome de usuário não pode ser vazio.\n        msg_range: Nome de usuário até 30 caracteres.\n        character: 'Deve usar o conjunto de caracteres \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Perfil Imagem\n        gravatar: Gravatar\n        gravatar_text: Você pode mudar a imagem em <1>gravatar.com</1>\n        custom: Customizado\n        btn_refresh: Atualizar\n        custom_text: Você pode enviar a sua image.\n        default: System\n        msg: Por favor envie um avatar\n      bio:\n        label: Sobre mim (opcional)\n      website:\n        label: Website (opcional)\n        placeholder: \"https://exemplo.com.br\"\n        msg: Formato incorreto de endereço de Website\n      location:\n        label: Localização (opcional)\n        placeholder: \"Cidade, País\"\n    notification:\n      Cabeçalho: Notificações\n      email:\n        label: E-mail Notificações\n        radio: \"Responda as suas perguntas, comentários, e mais\"\n    account:\n      Cabeçalho: Conta\n      change_email_btn: Mudar e-mail\n      change_pass_btn: Mudar senha\n      change_email_info: >-\n        Enviamos um e-mail para esse endereço. Siga as instruções de confirmação.\n      email:\n        label: E-mail\n        msg: E-mail não pode ser vazio.\n      password_title: Senha\n      current_pass:\n        label: Senha atual\n        msg:\n          empty: A Senha não pode ser vazia.\n          length: O comprimento deve estar entre 8 and 32.\n          different: As duas senhas inseridas não correspondem.\n      new_pass:\n        label: Nova Senha\n      pass_confirm:\n        label: Confirmar nova Senha\n    interface:\n      Cabeçalho: Interface\n      lang:\n        label: Idioma da Interface\n        text: Idioma da interface do usuário. A interface mudará quando você atualizar a página.\n  toast:\n    update: atualização realizada com sucesso\n    update_password: Senha alterada com sucesso.\n    flag_success: Obrigado por marcar.\n    forbidden_operate_self: Proibido para operar por você mesmo\n    review: A sua resposta irá aparecer após a revisão.\n  related_question:\n    title: Perguntas relacionadas\n    btn: Adicionar pegunta\n    answers: respostas\n  question_detail:\n    Perguntado: Perguntado\n    asked: perguntado\n    update: Modificado\n    edit: modificado\n    Views: Visualizado\n    Seguir: Seguir\n    Seguindo: Seguindo\n    answered: respondido\n    closed_in: Fechado em\n    show_exist: Mostrar pergunta existente.\n    answers:\n      title: Respostas\n      score: Pontuação\n      newest: Mais recente\n      btn_accept: Aceito\n      btn_accepted: Aceito\n    write_answer:\n      title: A sua Resposta\n      btn_name: Publicação a sua resposta\n      add_another_answer: Adicionar outra resposta\n      confirm_title: Continuar a responder\n      continue: Continuar\n      confirm_info: >-\n        <p>Tem certeza de que deseja adicionar outra resposta?</p><p>Você pode usar o link de edição para refinar e melhorar uma resposta existente.</p>\n      empty: Resposta não pode ser vazio.\n      caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento.\n    reopen:\n      title: Reabrir esta postagem\n      content: Tem certeza que deseja reabrir?\n      success: Esta postagem foi reaberta\n  delete:\n    title: Excluir esta postagem\n    question: >-\n      Nós não recomendamos <strong> excluir perguntas com respostas</strong> porque isso priva os futuros leitores desse conhecimento.</p><p>A exclusão repetida de perguntas respondidas pode resultar no bloqueio de perguntas de sua conta. Você tem certeza que deseja excluir?\n    answer_accepted: >-\n      <p>Não recomendamos <strong>excluir resposta aceita</strong> porque isso priva os futuros leitores desse conhecimento. </p> A exclusão repetida de respostas aceitas pode resultar no bloqueio de respostas de uma conta sua. Você tem certeza que deseja excluir?\n    other: Você tem certeza que deseja deletar?\n    tip_question_deleted: Esta postagem foi deletada\n    tip_answer_deleted: Esta resposta foi deletada\n  btns:\n    confirm: Confirmar\n    cancel: Cancelar\n    save: Salvar\n    delete: Deletar\n    login: Entrar\n    signup: Cadastrar-se\n    logout: Sair\n    verify: Verificar\n    add_question: Adicionar pergunta\n    approve: Aprovar\n    reject: Rejetar\n    skip: Pular\n  search:\n    title: Procurar Resultados\n    keywords: Palavras-chave\n    options: Opções\n    follow: Seguir\n    following: Seguindo\n    counts: \"{{count}} Resultados\"\n    more: Mais\n    sort_btns:\n      relevance: Relevância\n      newest: Mais recente\n      active: Ativar\n      score: Pontuação\n      more: Mais\n    tips:\n      title: Dicas de Pesquisa Avançada\n      tag: \"<1>[tag]</1> pesquisar dentro de um marcador\"\n      user: \"<1>user:username</1> buscar por autor\"\n      answer: \"<1>answers:0</1> perguntas não respondidas\"\n      score: \"<1>score:3</1> postagens com mais de 3+ placares\"\n      question: \"<1>is:question</1> buscar perguntas\"\n      is_answer: \"<1>is:answer</1> buscar respostas\"\n    empty: Não conseguimos encontrar nada. <br /> Tente palavras-chave diferentes ou menos específicas.\n  share:\n    name: Compartilhar\n    copy: Copiar link\n    via: Compartilhar postagem via...\n    copied: Copiado\n    facebook: Compartilhar no Facebook\n    twitter: Compartilhar no X\n  cannot_vote_for_self: Você não pode votar na sua própria postagem\n  modal_confirm:\n    title: Erro...\n  account_result:\n    page_title: Bem vindo ao {{site_name}}\n    success: A sua nova conta está confirmada; você será redirecionado para a página inicial.\n    link: Continuar para a página inicial.\n    invalid: >-\n      Desculpe, este link de confirmação não é mais válido. Talvez a sua já está ativa.\n    confirm_new_email: Seu e-mail foi atualizado.\n    confirm_new_email_invalid: >-\n      Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado.\n  unsubscribe:\n    page_title: Cancelar subscrição\n    success_title: Cancelamento de inscrição bem-sucedido\n    success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso.\n    link: Mudar configurações\n  question:\n    following_tags: Seguindo Marcadores\n    edit: Editar\n    save: Salvar\n    follow_tag_tip: Siga as tags para selecionar sua lista de perguntas.\n    hot_questions: Perguntas quentes\n    all_questions: Todas Perguntas\n    x_questions: \"{{ count }} perguntas\"\n    x_answers: \"{{ count }} respostas\"\n    questions: Perguntas\n    answers: Respostas\n    newest: Mais recente\n    active: Ativo\n    hot: Hot\n    score: Pontuação\n    unanswered: Não Respondido\n    modified: modificado\n    answered: respondido\n    asked: perguntado\n    closed: fechado\n    follow_a_tag: Seguir o marcador\n    more: Mais\n  personal:\n    overview: Visão geral\n    answers: Respostas\n    answer: resposta\n    questions: Perguntas\n    question: pergunta\n    bookmarks: Favoritas\n    reputation: Reputação\n    comments: Comentários\n    Votos: Votos\n    newest: Mais recente\n    score: Pontuação\n    edit_profile: Editar Perfil\n    visited_x_days: \"Visitado {{ count }} dias\"\n    viewed: Visualizado\n    joined: Ingressou\n    last_login: Visto\n    about_me: Sobre mim\n    about_me_empty: \"// Olá, Mundo !\"\n    top_answers: Melhores Respostas\n    top_questions: Melhores Perguntas\n    stats: Estatísticas\n    list_empty: Postagens não encontradas.<br />Talvez você queira selecionar uma guia diferente?\n    accepted: Aceito\n    answered: respondido\n    asked: perguntado\n    upvote: voto positivo\n    downvote: voto negativo\n    mod_short: Moderador\n    mod_long: Moderadores\n    x_reputation: reputação\n    x_Votos: votos recebidos\n    x_answers: respostas\n    x_questions: perguntas\n  install:\n    title: Instalação\n    next: Próximo\n    done: Completo\n    config_yaml_error: Não é possível criar o arquivo config.yaml.\n    lang:\n      label: Por favor Escolha um Idioma\n    db_type:\n      label: Mecanismo de banco de dados\n    db_username:\n      label: Nome de usuário\n      placeholder: root\n      msg: Nome de usuário não pode ser vazio.\n    db_password:\n      label: Senha\n      placeholder: root\n      msg: Senha não pode ser vazio.\n    db_host:\n      label: Host do banco de dados\n      placeholder: \"db:3306\"\n      msg: Host de banco de dados não pode ficar vazio.\n    db_name:\n      label: Nome do banco de dados\n      placeholder: answer\n      msg: O nome do banco de dados não pode ficar vazio.\n    db_file:\n      label: Arquivo de banco de dados\n      placeholder: /data/answer.db\n      msg: O arquivo de banco de dados não pode ficar vazio.\n    config_yaml:\n      title: Criar config.yaml\n      label: O arquivo config.yaml foi criado.\n      desc: >-\n        Você pode criar o arquivo <1>config.yaml</1> manualmente no diretório <1>/var/www/xxx/</1> e colar o seguinte texto nele.\n      info: Depois de fazer isso, clique no botão \"Avançar\".\n    site_information: Informações do site\n    admin_account: Administrador Conta\n    site_name:\n      label: Site Nome\n      msg: Site Nome não pode ser vazio.\n    site_url:\n      label: Site URL\n      text: O endereço do seu site.\n      msg:\n        empty: Site URL não pode ser vazio.\n        incorrect: Formato incorreto da URL do site.\n    contact_email:\n      label: E-mail para contato\n      text: Endereço de e-mail do contato principal responsável por este site.\n      msg:\n        empty: E-mail para contato não pode ser vazio.\n        incorrect: E-mail para contato em formato incorreto.\n    admin_name:\n      label: Nome\n      msg: Nome não pode ser vazio.\n    admin_password:\n      label: Senha\n      text: >-\n        Você precisará dessa senha para efetuar login. Por favor, guarde-a em um local seguro.\n      msg: Senha não pode ser vazia.\n    admin_email:\n      label: Email\n      text: Você precisará deste e-mail para fazer login.\n      msg:\n        empty: Email não pode ser vazio.\n        incorrect: Formato de e-mail incorreto.\n    ready_title: Sua resposta está pronta!\n    ready_desc: >-\n      Se você quiser alterar mais configurações, visite a <1>seção de administração</1>; encontre-a no menu do site.\n    good_luck: \"Divirta-se e boa sorte!\"\n    warn_title: Aviso\n    warn_desc: >-\n      O arquivo <1>config.yaml</1> já existe. Se precisar redefinir algum item de configuração neste arquivo, exclua-o primeiro.\n    install_now: Você pode tentar <1>instalar agora</1>.\n    installed: Já instalado\n    installed_desc: >-\n      Parece que você já instalou. Para reinstalar, limpe primeiro as tabelas antigas do seu banco de dados.\n    db_failed: Falha na conexão do banco de dados\n    db_failed_desc: >-\n      Isso significa que as informações do banco de dados em um arquivo <1>config.yaml</1> do SUA estão incorretas ou que o contato com o servidor do banco de dados não pôde ser estabelecido. Isso pode significar que o servidor de banco de dados de um host SUA está inativo.\n  counts:\n    views: visualizações\n    Votos: votos\n    answers: respostas\n    accepted: Aceito\n  page_404:\n    desc: \"Infelizmente, esta postagem não existe mais.\"\n    back_home: Voltar para a página inicial\n  page_50X:\n    desc: O servidor encontrou um erro e não pôde concluir sua solicitação.\n    back_home: Voltar para a página inicial\n  page_maintenance:\n    desc: \"Estamos em manutenção, voltaremos em breve.\"\n  nav_menus:\n    dashboard: Painel\n    contents: Conteúdos\n    questions: Perguntas\n    answers: Respostas\n    users: Usuários\n    flags: Marcadores\n    settings: Configurações\n    general: Geral\n    interface: Interface\n    smtp: SMTP\n    branding: Marca\n    legal: Legal\n    write: Escrever\n    tos: Termos de Serviços\n    privacy: Privacidade\n    seo: SEO\n    customize: Personalização\n    Temas: Temas\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Administrador\n    dashboard:\n      title: Painel\n      welcome: Bem vindo ao Administrador!\n      site_statistics: Estatísticas do Site\n      questions: \"Perguntas:\"\n      answers: \"Respostas:\"\n      comments: \"Comentários:\"\n      Votos: \"Votos:\"\n      active_users: \"Usuários ativos:\"\n      flags: \"Marcadores:\"\n      site_health_status: Estatísticas da saúde do site\n      version: \"Versão:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Enviando arquivos:\"\n      smtp: \"SMTP:\"\n      timezone: \"Fuso horário:\"\n      system_info: Informações do Sistema\n      storage_used: \"Armazenamento usado:\"\n      uptime: \"Tempo de atividade:\"\n      answer_links: Links das Respostas\n      documents: Documentos\n      feedback: Opinião\n      support: Suporte\n      review: Revisar\n      config: Configurações\n      update_to: Atualizar ao\n      latest: Ultimo\n      check_failed: Falha na verificação\n      \"yes\": \"Sim\"\n      \"no\": \"Não\"\n      not_allowed: Não permitido\n      allowed: Permitido\n      enabled: Ativo\n      disabled: Disponível\n    flags:\n      title: Marcadores\n      pending: Pendente\n      completed: Completo\n      flagged: Marcado\n      created: Criado\n      action: Ação\n      review: Revisar\n    change_modal:\n      title: Mudar user status to...\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n      normal_name: normal\n      normal_desc: Um usuário normal pode fazer e responder perguntas.\n      suspended_name: suspenso\n      suspended_desc: Um usuário suspenso não pode fazer login.\n      deleted_name: removido\n      deleted_desc: \"Deletar perfil, associações de autenticação.\"\n      inactive_name: inativo\n      inactive_desc: Um usuário inativo deve revalidar seu e-mail.\n      confirm_title: Remover este usuário\n      confirm_content: Tem certeza de que deseja excluir este usuário? Isso é permanente!\n      confirm_btn: Deletar\n      msg:\n        empty: Por favor selecione um motivo.\n    status_modal:\n      title: \"Mudar {{ type }} status para...\"\n      normal_name: normal\n      normal_desc: Um post normal disponível para todos.\n      closed_name: fechado\n      closed_desc: \"Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar.\"\n      deleted_name: removido\n      deleted_desc: Toda reputação ganha e perdida será restaurada.\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n      btn_next: Próximo\n    user_role_modal:\n      title: Altere a função do usuário para...\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    users:\n      title: Usuários\n      name: Nome\n      email: Email\n      reputation: Reputação\n      created_at: Hora de criação\n      delete_at: Hora da remoção\n      suspend_at: Hora da suspensão\n      status: Status\n      role: Função\n      action: Ação\n      change: Mudar\n      all: Todos\n      staff: Funcionários\n      inactive: Inativo\n      suspended: Suspenso\n      deleted: Removido\n      normal: Normal\n      Moderador: Moderador\n      Administrador: Administrador\n      Usuário: Usuário\n      filter:\n        placeholder: \"Filtrar por nome, user:id\"\n      set_new_password: Configurar nova senha\n      change_status: Mudar status\n      change_role: Mudar função\n      show_logs: Mostrar registros\n      add_user: Adicionar usuário\n      new_password_modal:\n        title: Configurar nova senha\n        form:\n          fields:\n            password:\n              label: Senha\n              text: O usuário será desconectado e precisará fazer login novamente.\n              msg: Senha de ver entre 8-32 caracteres em comprimento.\n        btn_cancel: Cancelar\n        btn_submit: Enviar\n      user_modal:\n        title: Adicionar novo usuário\n        form:\n          fields:\n            display_name:\n              label: Nome de exibição\n              msg: Nome de exibição deve ser 2-30 caracteres em comprimento.\n            email:\n              label: Email\n              msg: Email não é válido.\n            password:\n              label: Senha\n              msg: Senha deve ser 8-32 caracteres em comprimento.\n        btn_cancel: Cancelar\n        btn_submit: Enviar\n    questions:\n      page_title: Perguntas\n      normal: Normal\n      closed: Fechado\n      deleted: Removido\n      post: Publicação\n      Votos: Votos\n      answers: Respostas\n      created: Criado\n      status: Status\n      action: Ação\n      change: Mudar\n      filter:\n        placeholder: \"Filtrar por título, question:id\"\n    answers:\n      page_title: Respostas\n      normal: Normal\n      deleted: Removido\n      post: Publicação\n      Votos: Votos\n      created: Criado\n      status: Status\n      action: Ação\n      change: Mudar\n      filter:\n        placeholder: \"Filtrar por título, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Nome\n        msg: Site name não pode ser vazio.\n        text: \"O nome deste site, conforme usado na tag de título.\"\n      site_url:\n        label: URL do Site\n        msg: Site url não pode ser vazio.\n        validate: Por favor digite uma URL válida.\n        text: O endereço do seu site.\n      short_desc:\n        label: Breve Descrição do site (opcional)\n        msg: Breve Descrição do site não pode ser vazio.\n        text: \"Breve descrição, conforme usado na tag de título na página inicial.\"\n      desc:\n        label: Site Descrição (opcional)\n        msg: Descrição do site não pode ser vazio.\n        text: \"Descreva este site em uma única sentença, conforme usado na meta tag de descrição.\"\n      contact_email:\n        label: E-mail para contato\n        msg: E-mail par contato não pode ser vazio.\n        validate: E-mail par contato não é válido.\n        text: Endereço de e-mail do principal contato responsável por este site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (opcional)\n        msg: Site logo não pode ser vazio.\n        text: Você pode enviar a sua image ou <1>reiniciar</1> ao texto do título do site.\n      Tema:\n        label: Tema\n        msg: Tema não pode ser vazio.\n        text: Selecione um tema existente.\n      language:\n        label: Idioma da interface\n        msg: Idioma da Interface não pode ser vazio.\n        text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página.\n      time_zone:\n        label: Fuso horário\n        msg: Fuso horário não pode ser vazio.\n        text: Escolha a cidade no mesmo fuso horário que você.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: E-mail de origem\n        msg: E-mail de origem não pode ser vazio.\n        text: O endereço de e-mail de onde os e-mails são enviados.\n      from_name:\n        label: Nome de origem\n        msg: Nome de origem não pode ser vazio.\n        text: O nome de onde os e-mails são enviados.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host não pode ser vazio.\n        text: O seu servidor de e-mails.\n      encryption:\n        label: Criptografia\n        msg: Criptografia não pode ser vazio.\n        text: Para a maioria dos servidores SSL é a opção recomendada.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: A porta para seu servidor de e-mail.\n      smtp_username:\n        label: SMTP Nome de usuário\n        msg: SMTP username não pode ser vazio.\n      smtp_password:\n        label: SMTP Senha\n        msg: SMTP password não pode ser vazio.\n      test_email_recipient:\n        label: Destinatários de e-mail de teste\n        text: Forneça o endereço de e-mail que receberá os envios de testes.\n        msg: Os destinatários do e-mail de teste são inválidos\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication não pode ser vazio.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (opcional)\n        msg: Logo não pode ser vazio.\n        text: A imagem do logotipo no canto superior esquerdo do seu site. Use uma imagem retangular larga com altura de 56 e proporção maior que 3:1. Se deixada em branco, o texto do título do site será exibido.\n      mobile_logo:\n        label: Mobile Logo (opcional)\n        text: O logotipo usado na versão mobile do seu site. Use uma imagem retangular larga com altura de 56. Se deixado em branco, a imagem da configuração \"logotipo\" será usada.\n      square_icon:\n        label: Square Icon (opcional)\n        msg: Square icon não pode ser vazio.\n        text: Imagem usada como base para ícones de metadados. Idealmente, deve ser maior que 512x512.\n      favicon:\n        label: Favicon (opcional)\n        text: Um favicon para o seu site. Para funcionar corretamente em uma CDN, ele deve ser um png. Será redimensionado para 32x32. Se deixado em branco, o \"ícone quadrado\" será usado.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Termos de Serviço\n        text: \"Você pode adicionar conteúdo dos termos de serviço aqui. Se você já possui um documento hospedado em outro lugar, informe o URL completo aqui.\"\n      privacy_policy:\n        label: Política de Privacidade\n        text: \"Você pode adicionar o conteúdo da política de privacidade aqui. Se você já possui um documento hospedado em outro lugar, informe o URL completo aqui.\"\n    write:\n      page_title: Escrever\n      recommend_tags:\n        label: Recomendar Marcadores\n        text: \"Por favor, insira o slug da tag acima, uma tag por linha.\"\n      required_tag:\n        title: Tag necessária\n        label: Definir tag recomendada como necessária\n        text: \"Cada nova pergunta deve ter pelo menos uma tag de recomendação.\"\n      reserved_tags:\n        label: Marcadores Reservados\n        text: \"Tags reservadas só podem ser adicionadas a uma postagem pelo moderador.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Estruturas de URL personalizadas podem melhorar a usabilidade e a compatibilidade futura de seus links.\n      robots:\n        label: robots.txt\n        text: Isso substituirá permanentemente todas as configurações relacionadas do site.\n    Temas:\n      page_title: Temas\n      Temas:\n        label: Temas\n        text: Selecione um tema existente.\n      navbar_style:\n        label: Estilo da barra de navegação\n        text: Selecione um tema existente.\n      primary_color:\n        label: Cor primária\n        text: Modifique as cores usadas por seus Temas\n    css_and_html:\n      page_title: CSS e HTML\n      custom_css:\n        label: Custom CSS\n        text: Isto será inserido como <link>\n      head:\n        label: Head\n        text: Isto será inserido antes de </head>\n      header:\n        label: Header\n        text: Isto será inserido após <body>\n      footer:\n        label: Footer\n        text: Isso será inserido antes de </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Associação\n        label: Permitir novos registros\n        text: Desative para impedir que alguém crie uma nova conta.\n      private:\n        title: Privado\n        label: Login requirido\n        text: Somente usuários logados podem acessar esta comunidade.\n  form:\n    empty: não pode ser vazio\n    invalid: é inválido\n    btn_submit: Salvar\n    not_found_props: \"Propriedade necessária {{ key }} não encontrada.\"\n  page_review:\n    review: Revisar\n    proposed: proposta\n    question_edit: Editar Pergunta\n    answer_edit: Editar Resposta \n    tag_edit: Editar Tag\n    edit_summary: Editar resumo\n    edit_question: Editar pergunta\n    edit_answer: Editar resposta\n    edit_tag: Editar tag\n    empty: Não há mais tarefas de revisão.\n  timeline:\n    undeleted: não excluído\n    deleted: apagado\n    downvote: voto negativo\n    upvote: voto positivo\n    accept: aceitar\n    cancelled: cancelado\n    commented: comentado\n    rollback: rollback\n    edited: editado\n    answered: respondido\n    asked: perguntado\n    closed: fechado\n    reopened: reaberto\n    created: criado\n    title: \"Histórico para\"\n    tag_title: \"Linha do tempo para\"\n    show_Votos: \"Mostrar votos\"\n    n_or_a: N/A\n    title_for_question: \"Linha do tempo para\"\n    title_for_answer: \"Linha do tempo para resposta a {{ title }} por {{ author }}\"\n    title_for_tag: \"Linha do tempo para tag\"\n    datetime: Datetime\n    type: Tipo\n    by: Por\n    comment: Comentário\n    no_data: \"Não conseguimos encontrar nada.\"\n  users:\n    title: Usuários\n    users_with_the_most_reputation: Usuários com as maiores pontuações de reputação\n    users_with_the_most_vote: Usuários que mais votaram\n    staffs: Nossa equipe comunitária\n    reputation: reputação\n    Votos: Votos\n"
  },
  {
    "path": "i18n/pt_PT.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Sucesso.\n    unknown:\n      other: Erro desconhecido.\n    request_format_error:\n      other: Formato de solicitação inválido.\n    unauthorized_error:\n      other: Não autorizado.\n    database_error:\n      other: Erro no servidor de dados.\n    forbidden_error:\n      other: Proibido.\n    duplicate_request_error:\n      other: Solicitação duplicada.\n  action:\n    report:\n      other: Bandeira\n    edit:\n      other: Editar\n    delete:\n      other: Excluir\n    close:\n      other: Fechar\n    reopen:\n      other: Reabrir\n    forbidden_error:\n      other: Proibido.\n    pin:\n      other: Fixar\n    hide:\n      other: Não listar\n    unpin:\n      other: Desafixar\n    show:\n      other: Lista\n    invite_someone_to_answer:\n      other: Editar\n    undelete:\n      other: Desfazer\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Usuário\n      admin:\n        other: Administrador\n      moderator:\n        other: Moderador\n    description:\n      user:\n        other: Padrão sem acesso especial.\n      admin:\n        other: Possui acesso total.\n      moderator:\n        other: Acesso a todas as postagens, exceto configurações administrativas.\n  privilege:\n    level_1:\n      description:\n        other: Nível 1 (Menos reputação necessária para a equipe privado, grupo)\n    level_2:\n      description:\n        other: Nível 2 (pouca reputação necessária para a comunidade de inicialização)\n    level_3:\n      description:\n        other: Nível 3 (Alta reputação necessária para a comunidade adulta)\n    level_custom:\n      description:\n        other: Nível customizado\n    rank_question_add_label:\n      other: Perguntar\n    rank_answer_add_label:\n      other: Escrever resposta\n    rank_comment_add_label:\n      other: Comentar\n    rank_report_add_label:\n      other: Bandeira\n    rank_comment_vote_up_label:\n      other: Aprovar um comentário\n    rank_link_url_limit_label:\n      other: Poste mais de 2 links por vez\n    rank_question_vote_up_label:\n      other: Avaliar perguntar\n    rank_answer_vote_up_label:\n      other: Votar na resposta\n    rank_question_vote_down_label:\n      other: Desaprovar pergunta\n    rank_answer_vote_down_label:\n      other: Desaprovar resposta\n    rank_invite_someone_to_answer_label:\n      other: Convide alguém para responder\n    rank_tag_add_label:\n      other: Criar marcador\n    rank_tag_edit_label:\n      other: Editar descrição de um marcador (revisão necessária)\n    rank_question_edit_label:\n      other: Editar pergunta do outro (revisão necessária)\n    rank_answer_edit_label:\n      other: Editar a resposta do outro (revisão necessária)\n    rank_question_edit_without_review_label:\n      other: Editar a pergunta do outro sem revisar\n    rank_answer_edit_without_review_label:\n      other: Editar a resposta do outro sem revisar\n    rank_question_audit_label:\n      other: Rever edições de perguntas\n    rank_answer_audit_label:\n      other: Revisar edições de respostas\n    rank_tag_audit_label:\n      other: Revisar edições de marcadores\n    rank_tag_edit_without_review_label:\n      other: Editar descrições de marcadores sem revisar\n    rank_tag_synonym_label:\n      other: Gerenciar sinônimos de marcação\n  email:\n    other: E-mail\n  e_mail:\n    other: Email\n  password:\n    other: Senha\n  pass:\n    other: Senha\n  old_pass:\n    other: Current password\n  original_text:\n    other: Esta publicação\n  email_or_password_wrong_error:\n    other: O e-mail e a palavra-passe não coincidem.\n  error:\n    common:\n      invalid_url:\n        other: URL inválida.\n      status_invalid:\n        other: Estado inválido.\n    password:\n      space_invalid:\n        other: A senha não pode conter espaços.\n    admin:\n      cannot_update_their_password:\n        other: Você não pode modificar sua senha.\n      cannot_edit_their_profile:\n        other: Você não pode alterar o seu perfil.\n      cannot_modify_self_status:\n        other: Você não pode modificar seu status\n      email_or_password_wrong:\n        other: O e-mail e a palavra-passe não coincidem.\n    answer:\n      not_found:\n        other: Resposta não encontrada.\n      cannot_deleted:\n        other: Sem permissão para remover.\n      cannot_update:\n        other: Sem permissão para atualizar.\n      question_closed_cannot_add:\n        other: Perguntas são fechadas e não podem ser adicionadas.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Não é possível alterar comentários.\n      not_found:\n        other: Comentário não encontrado.\n      cannot_edit_after_deadline:\n        other: O tempo do comentário foi muito longo para ser modificado.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: O e-mail já existe.\n      need_to_be_verified:\n        other: O e-mail deve ser verificado.\n      verify_url_expired:\n        other: O e-mail verificado URL expirou, por favor, reenvie o e-mail.\n      illegal_email_domain_error:\n        other: O email não é permitido a partir desse email de domínio. Por favor, use outro.\n    lang:\n      not_found:\n        other: Arquivo de idioma não encontrado.\n    object:\n      captcha_verification_failed:\n        other: O Captcha está incorreto.\n      disallow_follow:\n        other: Você não tem permissão para seguir.\n      disallow_vote:\n        other: Você não possui permissão para votar.\n      disallow_vote_your_self:\n        other: Você não pode votar na sua própria postagem.\n      not_found:\n        other: Objeto não encontrado.\n      verification_failed:\n        other: A verificação falhou.\n      email_or_password_incorrect:\n        other: O e-mail e a senha não correspondem.\n      old_password_verification_failed:\n        other: Falha na verificação de senha antiga\n      new_password_same_as_previous_setting:\n        other: A nova senha é a mesma que a anterior.\n      already_deleted:\n        other: Esta publicação foi removida.\n    meta:\n      object_not_found:\n        other: Objeto meta não encontrado\n    question:\n      already_deleted:\n        other: Essa publicação foi deletado.\n      under_review:\n        other: Sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada.\n      not_found:\n        other: Pergunta não encontrada.\n      cannot_deleted:\n        other: Sem permissão para remover.\n      cannot_close:\n        other: Sem permissão para fechar.\n      cannot_update:\n        other: Sem permissão para atualizar.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: A classificação não atende à condição.\n      vote_fail_to_meet_the_condition:\n        other: Obrigado pela sugestão. Você precisa ser {{.Rank}} para poder votar.\n      no_enough_rank_to_operate:\n        other: Você precisa ser pelo menos {{.Rank}} para fazer isso.\n    report:\n      handle_failed:\n        other: Falha ao manusear relatório.\n      not_found:\n        other: Relatório não encontrado.\n    tag:\n      already_exist:\n        other: Marcação já existe.\n      not_found:\n        other: Marca não encontrada.\n      recommend_tag_not_found:\n        other: Marcador recomendado não existe.\n      recommend_tag_enter:\n        other: Por favor, insira pelo menos um marcador obrigatório.\n      not_contain_synonym_tags:\n        other: Não deve conter marcadores sinónimos.\n      cannot_update:\n        other: Sem permissão para atualizar.\n      is_used_cannot_delete:\n        other: Não é possível excluir um marcador em uso.\n      cannot_set_synonym_as_itself:\n        other: Você não pode definir o sinônimo do marcador atual como a si mesmo.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: O De Nome não pode ser um endereço de e-mail.\n    theme:\n      not_found:\n        other: Tema não encontrado.\n    revision:\n      review_underway:\n        other: Não é possível editar atualmente, há uma versão na fila de análise.\n      no_permission:\n        other: Sem pemissão para revisar.\n    user:\n      external_login_missing_user_id:\n        other: A plataforma de terceiros não fornece um ID único de usuário, então você não pode fazer login, entre em contato com o administrador do site.\n      external_login_unbinding_forbidden:\n        other: Por favor, defina uma senha de login para sua conta antes de remover esta conta.\n      email_or_password_wrong:\n        other:\n          other: O e-mail e a senha não conferem.\n      not_found:\n        other: Usuário não encontrado.\n      suspended:\n        other: O utilizador foi suspenso.\n      username_invalid:\n        other: Nome de usuário inválido.\n      username_duplicate:\n        other: O nome de usuário já em uso.\n      set_avatar:\n        other: Configuração de avatar falhou.\n      cannot_update_your_role:\n        other: Você não pode modificar a sua função.\n      not_allowed_registration:\n        other: Atualmente o site não está aberto para cadastro\n      not_allowed_login_via_password:\n        other: Atualmente o site não tem permissão para acessar utilizando senha.\n      access_denied:\n        other: Acesso negado\n      page_access_denied:\n        other: Você não tem permissão de acesso para esta página.\n      add_bulk_users_format_error:\n        other: \"Erro no formato {{.Field}} próximo '{{.Content}}' na linha {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"O número de usuários que você adicionou de uma vez deve estar no intervalo de 1 -{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Falha ao ler configuração\n    database:\n      connection_failed:\n        other: Falha ao conectar-se ao banco de dados\n      create_table_failed:\n        other: Falha ao criar tabela\n    install:\n      create_config_failed:\n        other: Não foi possível criar o arquivo de configuração.\n    upload:\n      unsupported_file_format:\n        other: Formato de arquivo não suportado.\n    site_info:\n      config_not_found:\n        other: Configuração do site não encontrada.\n    badge:\n      object_not_found:\n        other: Objeto emblema não encontrado\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Essa postagem é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual.\n    rude_or_abusive:\n      name:\n        other: rude ou abusivo\n      desc:\n        other: \"Uma pessoa razoável consideraria esse conteúdo inapropriado para um discurso respeitoso.\"\n    a_duplicate:\n      name:\n        other: uma duplicação\n      desc:\n        other: Esta pergunta já foi feita antes e já possui uma resposta.\n      placeholder:\n        other: Insira o link existente para a pergunta\n    not_a_answer:\n      name:\n        other: não é uma resposta\n      desc:\n        other: \"Foi apresentada como uma resposta, mas não tenta responder à pergunta. Talvez deva ser uma edição, um comentário, outra pergunta ou totalmente apagada.\"\n    no_longer_needed:\n      name:\n        other: não é mais necessário\n      desc:\n        other: Este comentário está desatualizado, conversacional ou não é relevante para esta publicação.\n    something:\n      name:\n        other: outro assunto\n      desc:\n        other: Esta postagem requer atenção da equipe por outra razão não listada acima.\n      placeholder:\n        other: Conte-nos especificamente com o que você está preocupado\n    community_specific:\n      name:\n        other: um motivo específico para a comunidade\n      desc:\n        other: Esta questão não atende a uma orientação da comunidade.\n    not_clarity:\n      name:\n        other: precisa de detalhes ou clareza\n      desc:\n        other: Atualmente esta pergunta inclui várias perguntas em um só. Deve se concentrar apenas em um problema.\n    looks_ok:\n      name:\n        other: parece estar OK\n      desc:\n        other: Este post é bom como está e não de baixa qualidade.\n    needs_edit:\n      name:\n        other: necessitava de edição, assim o fiz\n      desc:\n        other: Melhore e corrija problemas com este post você mesmo.\n    needs_close:\n      name:\n        other: precisa ser fechado\n      desc:\n        other: Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar.\n    needs_delete:\n      name:\n        other: precisa ser excluído\n      desc:\n        other: Esta postagem será excluída.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Esta pergunta já foi feita antes e já tem uma resposta.\n      guideline:\n        name:\n          other: um motivo específico da comunidade\n        desc:\n          other: Esta pergunta não atende a uma diretriz da comunidade.\n      multiple:\n        name:\n          other: precisa de detalhes ou clareza\n        desc:\n          other: Esta pergunta atualmente inclui várias perguntas em uma só. Por isso, deve focar em apenas um problema.\n      other:\n        name:\n          other: algo mais\n        desc:\n          other: Este post requer outro motivo não listado acima.\n    operation_type:\n      asked:\n        other: perguntado\n      answered:\n        other: respondido\n      modified:\n        other: modificado\n    deleted_title:\n      other: Questão excluída\n    questions_title:\n      other: Questões\n  tag:\n    tags_title:\n      other: Marcadores\n    no_description:\n      other: O marcador não possui descrição.\n  notification:\n    action:\n      update_question:\n        other: pergunta atualizada\n      answer_the_question:\n        other: pergunta respondida\n      update_answer:\n        other: resposta atualizada\n      accept_answer:\n        other: resposta aceita\n      comment_question:\n        other: pergunta comentada\n      comment_answer:\n        other: resposta comentada\n      reply_to_you:\n        other: respondeu a você\n      mention_you:\n        other: mencionou você\n      your_question_is_closed:\n        other: A sua pergunta foi fechada\n      your_question_was_deleted:\n        other: A sua pergunta foi deletada\n      your_answer_was_deleted:\n        other: A sua resposta foi deletada\n      your_comment_was_deleted:\n        other: O seu comentário foi deletado\n      up_voted_question:\n        other: votos positivos da pergunta\n      down_voted_question:\n        other: votos negativos da pergunta\n      up_voted_answer:\n        other: votos positivos da resposta\n      down_voted_answer:\n        other: votos negativos da resposta\n      up_voted_comment:\n        other: votos positivos do comentário\n      invited_you_to_answer:\n        other: lhe convidou para responder\n      earned_badge:\n        other: Ganhou o emblema \"{{.BadgeName}}\" emblema\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirme seu novo endereço de e-mail\"\n      body:\n        other: \"Confirme seu novo endereço de e-mail para {{.SiteName}} clicando no seguinte link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nSe você não solicitou esta alteração, por favor, ignore este email.<br><br>\\n\\n--<br>\\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} respondeu à sua pergunta\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Visualizar no {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancelar inscrição</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} convidou-lhe para responder\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>acho que você pode saber a resposta.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Visualizar na {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancelar Inscrição</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} comentou em sua publicação\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Visualizar no {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Cancelar inscrição</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Nova pergunta: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Redefinição de senha\"\n      body:\n        other: \"Alguém pediu para redefinir a sua senha em {{.SiteName}}.<br><br>\\n\\nSe não foi você, você pode ignorar este e-mail.<br><br>\\n\\nClique no seguinte link para escolher uma nova senha:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a><br><br>\\n\\n--<br>\\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirme seu novo endereço de e-mail\"\n      body:\n        other: \"Bem-vindo a {{.SiteName}}!<br><br>\\n\\nClique no seguinte link para confirmar e ativar sua nova conta:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nSe o link acima não for clicável, tente copiá-lo e colá-lo na barra de endereços do seu navegador web.\\n<br><br>\\n\\n--<br>\\nNota: Este é um e-mail de sistema automático. por favor, não responda a esta mensagem, pois a sua resposta não será vista.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] E-mail de teste\"\n      body:\n        other: \"Este é um e-mail de teste.\\n<br><br>\\n\\n--<br>\\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista.\"\n  action_activity_type:\n    upvote:\n      other: voto positivo\n    upvoted:\n      other: votos positivos\n    downvote:\n      other: voto negativo\n    downvoted:\n      other: voto negativo\n    accept:\n      other: aceito\n    accepted:\n      other: aceito\n    edit:\n      other: editar\n  review:\n    queued_post:\n      other: Publicação na fila\n    flagged_post:\n      other: Postagem sinalizada\n    suggested_post_edit:\n      other: Edições sugeridas\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} e mais {{ .Count }}...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiógrafo\n        desc:\n          other: Preenchido com <a href=\"{{ .ProfileURL }}\" target=\"_blank\">perfil</a> .\n      certified:\n        name:\n          other: Certificado\n        desc:\n          other: Completou o nosso tutorial de novo usuário.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: Primeira edição em publicação.\n      first_flag:\n        name:\n          other: Primeira Sinalização\n        desc:\n          other: Primeiro sinalização numa publicação.\n      first_upvote:\n        name:\n          other: Primeiro voto\n        desc:\n          other: Primeiro post votado.\n      first_link:\n        name:\n          other: Primeiro link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: Primeira Reação\n        desc:\n          other: Primeira reação a um post.\n      first_share:\n        name:\n          other: Primeiro Compartilhamento\n        desc:\n          other: Primeiro a compartilhar um post.\n      scholar:\n        name:\n          other: Académico\n        desc:\n          other: Fez uma pergunta e aceitou uma resposta.\n      commentator:\n        name:\n          other: Comentador\n        desc:\n          other: Fez 5 comentários.\n      new_user_of_the_month:\n        name:\n          other: Novo usuário do mês\n        desc:\n          other: Contribuições pendentes no seu primeiro mês.\n      read_guidelines:\n        name:\n          other: Ler diretrizes\n        desc:\n          other: Leia as [diretrizes da comunidade].\n      reader:\n        name:\n          other: Leitor\n        desc:\n          other: Leia todas as respostas num tópico com mais de 10 respostas.\n      welcome:\n        name:\n          other: Bem-vindo\n        desc:\n          other: Recebeu um voto positivo.\n      nice_share:\n        name:\n          other: Bom compartilhador\n        desc:\n          other: Compartilhou um post com 25 visitantes únicos.\n      good_share:\n        name:\n          other: Bom compartilhador\n        desc:\n          other: Compartilhou um post com 300 visitantes únicos.\n      great_share:\n        name:\n          other: Grande Compartilhador\n        desc:\n          other: Compartilhou um post com 1000 visitantes únicos.\n      out_of_love:\n        name:\n          other: Por amor\n        desc:\n          other: Cinquenta votos positivos em um dia.\n      higher_love:\n        name:\n          other: Amor Superior\n        desc:\n          other: Usou 50 votos positivos em um dia — 5 vezes.\n      crazy_in_love:\n        name:\n          other: Amor Louco\n        desc:\n          other: Usou 50 votos positivos em um dia — 20 vezes.\n      promoter:\n        name:\n          other: Promotor\n        desc:\n          other: Convidou um usuário.\n      campaigner:\n        name:\n          other: Ativista\n        desc:\n          other: Foram convidados 3 usuários básicos.\n      champion:\n        name:\n          other: Campeão\n        desc:\n          other: Cinco membros convidados.\n      thank_you:\n        name:\n          other: Obrigado\n        desc:\n          other: Recebeu 20 votos positivos e deu 10 votos.\n      gives_back:\n        name:\n          other: Dar de volta\n        desc:\n          other: Recebeu100 votos positivos e deu 100 votos a favor.\n      empathetic:\n        name:\n          other: Empático\n        desc:\n          other: Recebeu 500 votos e deu 1000 votos.\n      enthusiast:\n        name:\n          other: Entusiasta\n        desc:\n          other: .\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visitou 100 dias consecutivos.\n      devotee:\n        name:\n          other: Devoto\n        desc:\n          other: Visitou 365 dias consecutivos.\n      anniversary:\n        name:\n          other: Aniversário\n        desc:\n          other: Membro ativo por um ano, postando pelo menos uma vez.\n      appreciated:\n        name:\n          other: Apreciado\n        desc:\n          other: Recebeu 1 voto positivo em 20 posts.\n      respected:\n        name:\n          other: Respeitado\n        desc:\n          other: Novos 2 votos em 100 posts.\n      admired:\n        name:\n          other: Admirado\n        desc:\n          other: Recebeu 5 votos positivos em 300 posts.\n      solved:\n        name:\n          other: Resolvido\n        desc:\n          other: Ter uma resposta aceita.\n      guidance_counsellor:\n        name:\n          other: Orientador Educacional\n        desc:\n          other: Tenham 10 respostas aceitas.\n      know_it_all:\n        name:\n          other: Sabichão\n        desc:\n          other: Tenha 50 respostas aceitas.\n      solution_institution:\n        name:\n          other: Instituição de Soluções\n        desc:\n          other: Tenham 150 respostas aceitas.\n      nice_answer:\n        name:\n          other: Resposta legal\n        desc:\n          other: Resposta com pontuação maior que 10.\n      good_answer:\n        name:\n          other: Boa resposta\n        desc:\n          other: Resposta com pontuação maior que 25.\n      great_answer:\n        name:\n          other: Ótima Resposta\n        desc:\n          other: Resposta com pontuação maior que 50.\n      nice_question:\n        name:\n          other: Questão legal\n        desc:\n          other: Pergunta com pontuação maior que 10.\n      good_question:\n        name:\n          other: Boa questão\n        desc:\n          other: Questão com pontuação maior que 25.\n      great_question:\n        name:\n          other: Otima questão\n        desc:\n          other: Questão com pontuação maior que 50.\n      popular_question:\n        name:\n          other: Pergunta Popular\n        desc:\n          other: Pergunta com 500 visualizações.\n      notable_question:\n        name:\n          other: Pergunta Notável\n        desc:\n          other: Pergunta com 1000 visualizações.\n      famous_question:\n        name:\n          other: Pergunta Famosa\n        desc:\n          other: Pergunta com 5000 visualizações.\n      popular_link:\n        name:\n          other: Link Popular\n        desc:\n          other: Postou um link externo com 50 cliques.\n      hot_link:\n        name:\n          other: Link Quente\n        desc:\n          other: Postou um link externo com 300 cliques.\n      famous_link:\n        name:\n          other: Link Famoso\n        desc:\n          other: Postou um link externo com 100 cliques.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Começando\n      community:\n        name:\n          other: Comunidade\n      posting:\n        name:\n          other: Postando\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Como formatar\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mencionar uma postagem: <code>#post_id</code></p></li><li><p class=\"mb-2\">para mais links</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Título](https://url.com)</code></pre></li><li><p class=\"mb-2\">colocar retornos entre parágrafos</p></li><li><p class=\"mb-2\"><em>_italic_</em> ou **<strong>bold</strong>**</p></li><li><p class=\"mb-2\">identar código por 4 espaços</p></li><li><p class=\"mb-2\">cotação colocando <code>&gt;</code> no início da linha</p></li><li><p class=\"mb-2\">aspas invertidas <code>`tipo_isso`</code></p></li><li><p class=\"mb-2\">criar cercas de código com aspas invertidas <code>`</code></p><pre class=\"mb-0\"><code>```<br/>código aqui<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Anterior\n    next: Próximo\n  page_title:\n    question: Pergunta\n    questions: Perguntas\n    tag: Marcador\n    tags: Marcadores\n    tag_wiki: marcador wiki\n    create_tag: Criar Marcador\n    edit_tag: Editar Marcador\n    ask_a_question: Create Question\n    edit_question: Editar Pergunta\n    edit_answer: Editar Resposta\n    search: Busca\n    posts_containing: Postagens contendo\n    settings: Configurações\n    notifications: Notificações\n    login: Entrar\n    sign_up: Registar-se\n    account_recovery: Recuperação de conta\n    account_activation: Ativação de conta\n    confirm_email: Confirmar E-mail\n    account_suspended: Conta suspensa\n    admin: Administrador\n    change_email: Modificar e-mail\n    install: Instalação do Answer\n    upgrade: Atualização do Answer\n    maintenance: Manutenção do website\n    users: Usuários\n    oauth_callback: Processando\n    http_404: HTTP Erro 404\n    http_50X: HTTP Erro 500\n    http_403: HTTP Erro 403\n    logout: Encerrar Sessão\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notificações\n    inbox: Caixa de entrada\n    achievement: Conquistas\n    new_alerts: Novos alertas\n    all_read: Marcar todos como lida\n    show_more: Mostrar mais\n    someone: Alguém\n    inbox_type:\n      all: Tudo\n      posts: Postagens\n      invites: Convites\n      votes: Votos\n    answer: Resposta\n    question: Questão\n    badge_award: Emblema\n  suspended:\n    title: A sua conta foi suspensa\n    until_time: \"Sua conta está suspensa até {{ time }}.\"\n    forever: Este usuário foi suspenso permanentemente.\n    end: Você não atende a uma diretriz da comunidade.\n    contact_us: Entre em contato conosco\n  editor:\n    blockquote:\n      text: Bloco de citação\n    bold:\n      text: Negrito\n    chart:\n      text: Gráfico\n      flow_chart: Gráfico de fluxo\n      sequence_diagram: Diagrama de sequência\n      class_diagram: Diagrama de classe\n      state_diagram: Diagrama de estado\n      entity_relationship_diagram: Diagrama de relacionamento de entidade\n      user_defined_diagram: Diagrama definido pelo usuário\n      gantt_chart: Gráfico de Gantt\n      pie_chart: Gráfico de pizza\n    code:\n      text: Exemplo de código\n      add_code: Adicionar exemplo de código\n      form:\n        fields:\n          code:\n            label: Código\n            msg:\n              empty: Código não pode ser vazio.\n          language:\n            label: Idioma\n            placeholder: Deteção automática\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n    formula:\n      text: Fórmula\n      options:\n        inline: Fórmula na linha\n        block: Bloco de fórmula\n    heading:\n      text: Cabeçalho\n      options:\n        h1: Cabeçalho 1\n        h2: Cabeçalho 2\n        h3: Cabeçalho 3\n        h4: Cabeçalho 4\n        h5: Cabeçalho 5\n        h6: Cabeçalho 6\n    help:\n      text: Ajuda\n    hr:\n      text: Régua horizontal\n    image:\n      text: Imagem\n      add_image: Adicionar imagem\n      tab_image: Enviar image,\n      form_image:\n        fields:\n          file:\n            label: Arquivo de imagem\n            btn: Selecione imagem\n            msg:\n              empty: Arquivo não pode ser vazio.\n              only_image: Somente um arquivo de imagem é permitido.\n              max_size: O tamanho do arquivo não pode exceder {{size}} MB.\n          desc:\n            label: Descrição (opcional)\n      tab_url: URL da imagem\n      form_url:\n        fields:\n          url:\n            label: URL da imagem\n            msg:\n              empty: URL da imagem não pode ser vazia.\n          name:\n            label: Descrição (opcional)\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n      uploading: Enviando\n    indent:\n      text: Identação\n    outdent:\n      text: Não identado\n    italic:\n      text: Emphase\n    link:\n      text: Superlink (Hyperlink)\n      add_link: Adicionar superlink (hyperlink)\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL não pode ser vazia.\n          name:\n            label: Descrição (opcional)\n      btn_cancel: Cancelar\n      btn_confirm: Adicionar\n    ordered_list:\n      text: Lista numerada\n    unordered_list:\n      text: Lista com marcadores\n    table:\n      text: Tabela\n      heading: heading\n      cell: Célula\n    file:\n      text: Anexar arquivos\n      not_supported: \"Não suporta esse tipo de arquivo. Tente novamente com {{file_type}}.\"\n      max_size: \"Anexar arquivos não pode exceder {{size}} MB.\"\n  close_modal:\n    title: Estou fechando este post como...\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: Não pode ser vazio.\n    msg:\n      empty: Por favor selecione um motivo.\n  report_modal:\n    flag_title: Estou marcando para denunciar este post como...\n    close_title: Estou fechando este post como...\n    review_question_title: Revisar pergunta\n    review_answer_title: Revisar resposta\n    review_comment_title: Revisar comentário\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    remark:\n      empty: Não pode ser vazio.\n    msg:\n      empty: Por favor selecione um motivo.\n      not_a_url: Formato da URL incorreto.\n      url_not_match: A origem da URL não corresponde ao site atual.\n  tag_modal:\n    title: Criar novo marcador\n    form:\n      fields:\n        display_name:\n          label: Nome de exibição\n          msg:\n            empty: Nome de exibição não pode ser vazio.\n            range: Nome de exibição tem que ter até 35 caracteres.\n        slug_name:\n          label: Slug de URL\n          desc: 'Deve usar o conjunto de caracteres \"a-z\", \"0-9\", \"+ # - .\"'\n          msg:\n            empty: URL slug não pode ser vazio.\n            range: URL slug até 35 caracteres.\n            character: URL slug contém conjunto de caracteres não permitido.\n        desc:\n          label: Descrição (opcional)\n        revision:\n          label: Revisão\n        edit_summary:\n          label: Editar descrição\n          placeholder: >-\n            Explique resumidamente as suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_cancel: Cancelar\n    btn_submit: Enviar\n    btn_post: Postar novo marcador\n  tag_info:\n    created_at: Criado\n    edited_at: Editado\n    history: Histórico\n    synonyms:\n      title: Sinônimos\n      text: Os marcadores a seguir serão re-mapeados para\n      empty: Sinônimos não encotrados.\n      btn_add: Adicionar um sinônimo\n      btn_edit: Editar\n      btn_save: Salvar\n    synonyms_text: Os marcadores a seguir serão re-mapeados para\n    delete:\n      title: Remover este marcador\n      tip_with_posts: >-\n        <p>Nós não permitimos <strong>remover marcadores com postagens</strong>.</p> <p>Por favor, remova este marcador a partir da postagem.</p>\n      tip_with_synonyms: >-\n        <p>Nós não permitimos <strong>remover marcadores com postagens</strong>.</p> <p>Por favor, remova este marcador a partir da postagem.</p>\n      tip: Você tem certeza que deseja remover?\n      close: Fechar\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Editar marcador\n    default_reason: Editar marcador\n    default_first_reason: Adicionar marcador\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n  dates:\n    long_date: D MMM\n    long_date_with_year: \"D MMM, YYYY\"\n    long_date_with_time: \"D MMM, YYYY [at] HH:mm\"\n    now: agora\n    x_seconds_ago: \"{{count}}s atrás\"\n    x_minutes_ago: \"{{count}}m atrás\"\n    x_hours_ago: \"{{count}}h atrás\"\n    hour: hora\n    day: dia\n    hours: horas\n    days: dias\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: coração\n    smile: sorrir\n    frown: cara feia\n    btn_label: adicionar ou remover reações\n    undo_emoji: desfazer reação {{ emoji }}\n    react_emoji: reagir com {{ emoji }}\n    unreact_emoji: remover reação {{ emoji }}\n  comment:\n    btn_add_comment: Adicionar comentário\n    reply_to: Responder a\n    btn_reply: Responder\n    btn_edit: Editar\n    btn_delete: Excluir\n    btn_flag: Marcador\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n    show_more: \"Mais {{count}} comentários\"\n    tip_question: >-\n      Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários.\n    tip_answer: >-\n      Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar.\n    tip_vote: Isto adiciona alguma utilidade à postagem\n  edit_answer:\n    title: Editar Resposta\n    default_reason: Editar Resposta\n    default_first_reason: Adicionar resposta\n    form:\n      fields:\n        revision:\n          label: Revisão\n        answer:\n          label: Resposta\n          feedback:\n            characters: conteúdo deve ser pelo menos 6 characters em comprimento.\n        edit_summary:\n          label: Resumo da edição\n          placeholder: >-\n            Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_save_edits: Salvar edições\n    btn_cancel: Cancelar\n  tags:\n    title: Marcadores\n    sort_buttons:\n      popular: Popular\n      name: Nome\n      newest: mais recente\n    button_follow: Seguir\n    button_following: Seguindo\n    tag_label: perguntas\n    search_placeholder: Filtrar por nome de marcador\n    no_desc: O marcador não possui descrição.\n    more: Mais\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Editar Pergunta\n    default_reason: Editar pergunta\n    default_first_reason: Create question\n    similar_questions: Similar perguntas\n    form:\n      fields:\n        revision:\n          label: Revisão\n        title:\n          label: Título\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Título não pode ser vazio.\n            range: Título até 150 caracteres\n        body:\n          label: Corpo\n          msg:\n            empty: Corpo da mensagem não pode ser vazio.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Marcadores\n          msg:\n            empty: Marcadores não podes ser vazios.\n        answer:\n          label: Resposta\n          msg:\n            empty: Resposta não pode ser vazia.\n        edit_summary:\n          label: Resumo da edição\n          placeholder: >-\n            Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada)\n    btn_post_question: Publicação a sua pergunta\n    btn_save_edits: Salvar edições\n    answer_question: Responda a sua própria pergunta\n    post_question&answer: Publicação a sua pergunta e resposta\n  tag_selector:\n    add_btn: Adicionar marcador\n    create_btn: Criar novo marcador\n    search_tag: Procurar marcador\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Nenhum marcador correspondente\n    tag_required_text: Marcador obrigatório (ao menos um)\n  header:\n    nav:\n      question: Perguntas\n      tag: Marcadores\n      user: Usuários\n      badges: Emblemas\n      profile: Perfil\n      setting: Configurações\n      logout: Sair\n      admin: Administrador\n      review: Revisar\n      bookmark: Favoritos\n      moderation: Moderação\n    search:\n      placeholder: Procurar\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Mudar\n    loading: carregando...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Escreva o texto acima\n    msg:\n      empty: Captcha não pode ser vazio.\n  inactive:\n    first: >-\n      Você está quase pronto! Enviamos um e-mail de ativação para <bold>{{mail}}</bold>. Por favor, siga as instruções no e-mail para ativar uma conta sua.\n    info: \"Se não chegar, verifique sua pasta de spam.\"\n    another: >-\n      Enviamos outro e-mail de ativação para você em <bold>{{mail}}</bold>. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam.\n    btn_name: Reenviar e-mail de ativação\n    change_btn_name: Mudar email\n    msg:\n      empty: Não pode ser vazio.\n    resend_email:\n      url_label: Tem certeza de que deseja reenviar o e-mail de ativação?\n      url_text: Você também pode fornecer o link de ativação acima para o usuário.\n  login:\n    login_to_continue: Entre para continue\n    info_sign: Não possui uma conta? <1>Cadastrar-se</1>\n    info_login: Já possui uma conta? <1>Entre</1>\n    agreements: Ao se registrar, você concorda com as <1>políticas de privacidades</1> e os <3>termos de serviços</3>.\n    forgot_pass: Esqueceu a sua senha?\n    name:\n      label: Nome\n      msg:\n        empty: Nome não pode ser vazio.\n        range: O nome deve ter entre 2 e 30 caracteres.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n    password:\n      label: Senha\n      msg:\n        empty: Senha não pode ser vazia.\n        different: As senhas inseridas em ambos os campos são inconsistentes\n  account_forgot:\n    page_title: Esqueceu a sua senha\n    btn_name: Enviar e-mail de recuperação de senha\n    send_success: >-\n      Se uma conta corresponder <strong>{{mail}}</strong>, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve.\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n  change_email:\n    btn_cancel: Cancelar\n    btn_update: Atualiza email address\n    send_success: >-\n      Se uma conta corresponder <strong>{{mail}}</strong>, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve.\n    email:\n      label: Novo E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n  oauth:\n    connect: Conecte com {{ auth_name }}\n    remove: Remover {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Adicione um e-mail para recuperação da conta.\n    btn_update: Atualizar endereço de e-mail\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail não pode ser vazio.\n    modal_title: E-mail já existe.\n    modal_content: Este e-mail já está cadastrado. Você tem certeza que deseja conectar à conta existente?\n    modal_cancel: Alterar E-mail\n    modal_confirm: Conectar a uma conta existente\n  password_reset:\n    page_title: Redefinir senha\n    btn_name: Redefinir minha senha\n    reset_success: >-\n      Você alterou com sucesso uma senha sua; você será redirecionado para a página de login.\n    link_invalid: >-\n      Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida?\n    to_login: Continuar para a tela de login\n    password:\n      label: Senha\n      msg:\n        empty: Senha não pode ser vazio.\n        length: O comprimento deve estar entre 8 e 32\n        different: As senhas inseridas em ambos os campos são inconsistentes\n    password_confirm:\n      label: Confirmar Nova senha\n  settings:\n    page_title: Configurações\n    goto_modify: Ir para modificar\n    nav:\n      profile: Perfil\n      notification: Notificações\n      account: Conta\n      interface: Interface\n    profile:\n      heading: Perfil\n      btn_name: Salvar\n      display_name:\n        label: Nome de exibição\n        msg: Nome de exibição não pode ser vazio.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Nome de usuário\n        caption: As pessoas poderão mensionar você com \"@usuário\".\n        msg: Nome de usuário não pode ser vazio.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Perfil Imagem\n        gravatar: Gravatar\n        gravatar_text: Você pode mudar a imagem em <1>gravatar.com</1>\n        custom: Customizado\n        custom_text: Você pode enviar a sua image.\n        default: Padrão do Sistema\n        msg: Por favor envie um avatar\n      bio:\n        label: Sobre mim (opcional)\n      website:\n        label: Website (opcional)\n        placeholder: \"https://exemplo.com.br\"\n        msg: Formato incorreto de endereço de Website\n      location:\n        label: Localização (opcional)\n        placeholder: \"Cidade, País\"\n    notification:\n      heading: Notificações por e-mail\n      turn_on: Ativar\n      inbox:\n        label: Notificações na caixa de entrada\n        description: Responda suas próprias perguntas, comentários, convites e muito mais.\n      all_new_question:\n        label: Todas as perguntas novas\n        description: Seja notificado de todas as novas perguntas. Até 50 perguntas por semana.\n      all_new_question_for_following_tags:\n        label: Novas perguntas para os seguintes marcadores\n        description: Seja notificado de novas perguntas para os seguintes marcadores.\n    account:\n      heading: Conta\n      change_email_btn: Mudar e-mail\n      change_pass_btn: Mudar senha\n      change_email_info: >-\n        Enviamos um e-mail para esse endereço. Siga as instruções de confirmação.\n      email:\n        label: Correio eletrónico\n      new_email:\n        label: Novo correio eletrónico\n        msg: Novo correio eletrónico não pode ser vazio.\n      pass:\n        label: Senha atual\n        msg: Senha não pode ser vazio.\n      password_title: Senha\n      current_pass:\n        label: Senha atual\n        msg:\n          empty: A Senha não pode ser vazia.\n          length: O comprimento deve estar entre 8 and 32.\n          different: As duas senhas inseridas não correspondem.\n      new_pass:\n        label: Nova Senha\n      pass_confirm:\n        label: Confirmar nova Senha\n    interface:\n      heading: Interface\n      lang:\n        label: Idioma da Interface\n        text: Idioma da interface do usuário. A interface mudará quando você atualizar a página.\n    my_logins:\n      title: Meus logins\n      label: Entre ou cadastre-se neste site utilizando estas contas.\n      modal_title: Remover login\n      modal_content: Você tem certeza que deseja remover este login da sua conta?\n      modal_confirm_btn: Remover\n      remove_success: Removido com sucesso\n  toast:\n    update: atualização realizada com sucesso\n    update_password: Senha alterada com sucesso.\n    flag_success: Obrigado por marcar.\n    forbidden_operate_self: Proibido para operar por você mesmo\n    review: A sua resposta irá aparecer após a revisão.\n    sent_success: Enviado com sucesso\n  related_question:\n    title: Related\n    answers: respostas\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Pessoas Perguntaram\n    desc: Select people who you think might know the answer.\n    invite: Convidar para responder\n    add: Adicionar pessoas\n    search: Procurar pessoas\n  question_detail:\n    action: Acção\n    created: Created\n    Asked: Perguntado\n    asked: perguntado\n    update: Modificado\n    Edited: Edited\n    edit: modificado\n    commented: comentado\n    Views: Visualizado\n    Follow: Seguir\n    Following: Seguindo\n    follow_tip: Siga esta pergunta para receber notificações\n    answered: respondido\n    closed_in: Fechado em\n    show_exist: Mostrar pergunta existente.\n    useful: Útil\n    question_useful: Isso é útil e claro\n    question_un_useful: Isso não está claro ou não é útil\n    question_bookmark: Favoritar esta pergunta\n    answer_useful: Isso é útil\n    answer_un_useful: Isso não é útil\n    answers:\n      title: Respostas\n      score: Pontuação\n      newest: Mais recente\n      oldest: Mais Antigos\n      btn_accept: Aceito\n      btn_accepted: Aceito\n    write_answer:\n      title: A sua Resposta\n      edit_answer: Editar a minha resposta existente\n      btn_name: Publicação a sua resposta\n      add_another_answer: Adicionar outra resposta\n      confirm_title: Continuar a responder\n      continue: Continuar\n      confirm_info: >-\n        <p>Tem certeza de que deseja adicionar outra resposta?</p><p>Você pode usar o link de edição para refinar e melhorar uma resposta existente.</p>\n      empty: Resposta não pode ser vazio.\n      characters: conteúdo deve ser pelo menos 6 caracteres em comprimento.\n      tips:\n        header_1: Obrigado pela sua resposta\n        li1_1: Por favor, não esqueça de <strong>responder a pergunta</strong>. Providencie detalhes e compartilhe a sua pesquisa.\n        li1_2: Faça backup de quaisquer declarações que você fizer com referências ou experiência pessoal.\n        header_2: Mas <strong>evite</strong> ...\n        li2_1: Pedir ajuda, buscar esclarecimentos ou responder a outras respostas.\n    reopen:\n      confirm_btn: Reabrir\n      title: Reabrir esta postagem\n      content: Você tem certeza que deseja reabrir?\n    list:\n      confirm_btn: Lista\n      title: Liste esta postagem\n      content: Você tem certeza que deseja listar?\n    unlist:\n      confirm_btn: Remover da lista\n      title: Remover da lista de postagens\n      content: Tem certeza de que deseja remover da lista?\n    pin:\n      title: Fixe esta postagem\n      content: Tem certeza de que deseja fixar globalmente? Esta postagem aparecerá no topo de todas as listas de postagens.\n      confirm_btn: Fixar\n  delete:\n    title: Excluir esta postagem\n    question: >-\n      Nós não recomendamos <strong>excluindo perguntas com respostas</strong> porque isso priva os futuros leitores desse conhecimento.</p><p>Repeated deletion of answered questions can result in a sua account being blocked from asking. Você tem certeza que deseja deletar?\n    answer_accepted: >-\n      <p>Nós não recomendamos <strong>excluir perguntas com respostas</strong> porque isso priva os futuros leitores desse conhecimento. </p> A exclusão repetida de respostas aceitas pode resultar no bloqueio de respostas de sua conta.. Você tem certeza que deseja deletar?\n    other: Você tem certeza que deseja deletar?\n    tip_answer_deleted: Esta resposta foi deletada\n    undelete_title: Recuperar esta publicação\n    undelete_desc: Você tem certeza que deseja recuperar?\n  btns:\n    confirm: Confirmar\n    cancel: Cancelar\n    edit: Editar\n    save: Salvar\n    delete: Excluir\n    undelete: Recuperar\n    list: Lista\n    unlist: Não listar\n    unlisted: Não listado\n    login: Entrar\n    signup: Cadastrar-se\n    logout: Sair\n    verify: Verificar\n    create: Create\n    approve: Aprovar\n    reject: Rejetar\n    skip: Pular\n    discard_draft: Descartar rascunho\n    pinned: Fixado\n    all: Todos\n    question: Pergunta\n    answer: Resposta\n    comment: Comentário\n    refresh: Atualizar\n    resend: Reenviar\n    deactivate: Desativar\n    active: Ativar\n    suspend: Suspender\n    unsuspend: Suspensão cancelada\n    close: Fechar\n    reopen: Reabrir\n    ok: OK\n    light: Claro\n    dark: Escuro\n    system_setting: Definições de sistema\n    default: Padrão\n    reset: Reset\n    tag: Marcador\n    post_lowercase: publicação\n    filter: Filtro\n    ignore: Ignorar\n    submit: Submeter\n    normal: Normal\n    closed: Fechado\n    deleted: Removido\n    deleted_permanently: Deleted permanently\n    pending: Pendente\n    more: Mais\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Procurar Resultados\n    keywords: Palavras-chave\n    options: Opções\n    follow: Seguir\n    following: Seguindo\n    counts: \"{{count}} Resultados\"\n    counts_loading: \"... Results\"\n    more: Mais\n    sort_btns:\n      relevance: Relevância\n      newest: Mais recente\n      active: Ativar\n      score: Pontuação\n      more: Mais\n    tips:\n      title: Dicas de Pesquisa Avançada\n      tag: \"<1>[tag]</1> pesquisar com um marcador\"\n      user: \"<1>user:username</1> buscar por autor\"\n      answer: \"<1>answers:0</1> perguntas não respondidas\"\n      score: \"<1>score:3</1> postagens com mais de 3+ placares\"\n      question: \"<1>is:question</1> buscar perguntas\"\n      is_answer: \"<1>is:answer</1> buscar respostas\"\n    empty: Não conseguimos encontrar nada. <br /> Tente palavras-chave diferentes ou menos específicas.\n  share:\n    name: Compartilhar\n    copy: Copiar link\n    via: Compartilhar postagem via...\n    copied: Copiado\n    facebook: Compartilhar no Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Você não pode votar na sua própria postagem\n  modal_confirm:\n    title: Erro...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: A sua nova conta está confirmada; você será redirecionado para a página inicial.\n    link: Continuar para a página inicial.\n    oops: Oops!\n    invalid: O link utilizado não funciona mais.\n    confirm_new_email: O seu e-mail foi atualizado.\n    confirm_new_email_invalid: >-\n      Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado.\n  unsubscribe:\n    page_title: Cancelar subscrição\n    success_title: Cancelamento de inscrição bem-sucedido\n    success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso.\n    link: Mudar configurações\n  question:\n    following_tags: Seguindo Marcadores\n    edit: Editar\n    save: Salvar\n    follow_tag_tip: Seguir tags to curate a sua lista de perguntas.\n    hot_questions: Perguntas quentes\n    all_questions: Todas Perguntas\n    x_questions: \"{{ count }} perguntas\"\n    x_answers: \"{{ count }} respostas\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Perguntas\n    answers: Respostas\n    newest: Mais recente\n    active: Ativo\n    hot: Popular\n    frequent: Frequente\n    recommend: Recomendado\n    score: Pontuação\n    unanswered: Não Respondido\n    modified: modificado\n    answered: respondido\n    asked: perguntado\n    closed: fechado\n    follow_a_tag: Seguir o marcador\n    more: Mais\n  personal:\n    overview: Visão geral\n    answers: Respostas\n    answer: resposta\n    questions: Perguntas\n    question: pergunta\n    bookmarks: Favoritas\n    reputation: Reputação\n    comments: Comentários\n    votes: Votos\n    badges: Emblemas\n    newest: Mais recente\n    score: Pontuação\n    edit_profile: Editar Perfil\n    visited_x_days: \"Visitado {{ count }} dias\"\n    viewed: Visualizado\n    joined: Ingressou\n    comma: \",\"\n    last_login: Visto\n    about_me: Sobre mim\n    about_me_empty: \"// Olá, Mundo !\"\n    top_answers: Melhores Respostas\n    top_questions: Melhores Perguntas\n    stats: Estatísticas\n    list_empty: Postagens não encontradas.<br />Talvez você queira selecionar uma guia diferente?\n    content_empty: Nenhum post encontrado.\n    accepted: Aceito\n    answered: respondido\n    asked: perguntado\n    downvoted: voto negativo\n    mod_short: Moderador\n    mod_long: Moderadores\n    x_reputation: reputação\n    x_votes: votos recebidos\n    x_answers: respostas\n    x_questions: perguntas\n    recent_badges: Emblemas recentes\n  install:\n    title: Instalação\n    next: Proximo\n    done: Completo\n    config_yaml_error: Não é possível criar o arquivo config.yaml.\n    lang:\n      label: Por favor Escolha um Idioma\n    db_type:\n      label: Motor do Banco de dados\n    db_username:\n      label: Nome de usuário\n      placeholder: raiz\n      msg: Nome de usuário não pode ser vazio.\n    db_password:\n      label: Senha\n      placeholder: raiz\n      msg: Senha não pode ser vazio.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host não pode ser vazio.\n    db_name:\n      label: Database Nome\n      placeholder: resposta\n      msg: Database Nome não pode ser vazio.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File não pode ser vazio.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Criar config.yaml\n      label: Arquivo config.yaml criado.\n      desc: >-\n        Você pode criar o arquivo <1>config.yaml</1> manualmente no diretório <1>/var/ww/xxx/</1> e colar o seguinte texto nele.\n      info: Qlique no botão \"Próximo\" após finalizar.\n    site_information: Informação do Site\n    admin_account: Administrador Conta\n    site_name:\n      label: Site Nome\n      msg: Site Nome não pode ser vazio.\n      msg_max_length: O nome do site deve ter no máximo 30 caracteres.\n    site_url:\n      label: URL do Site\n      text: O endereço do seu site.\n      msg:\n        empty: Site URL não pode ser vazio.\n        incorrect: URL do site está incorreto.\n        max_length: O nome do site deve ter no máximo 512 caracteres.\n    contact_email:\n      label: E-mail par contato\n      text: O endereço de e-mail do contato principal deste site.\n      msg:\n        empty: E-mail par contato não pode ser vazio.\n        incorrect: E-mail par contato incorrect format.\n    login_required:\n      label: Privado\n      switch: É necessário fazer login\n      text: Somente usuários conectados podem acessar esta comunidade.\n    admin_name:\n      label: Nome\n      msg: Nome não pode ser vazio.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Senha\n      text: >-\n        You will need this password to log in. Por favor store it in a secure location.\n      msg: Senha não pode ser vazio.\n      msg_min_length: A senha deve ser ter pelo menos 8 caracteres.\n      msg_max_length: A senha deve ter no máximo 32 caracteres.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: E-mail\n      text: Você precisará deste e-mail para efetuar o login.\n      msg:\n        empty: Email não pode ser vazio.\n        incorrect: O formato do e-mail está incorreto.\n    ready_title: Seu site está pronto\n    ready_desc: >-\n      Se você quiser alterar mais configurações, visite <1>seção de administrador</1>; encontre-o no menu do site.\n    good_luck: \"Divirta-se, e boa sorte!\"\n    warn_title: Atenção\n    warn_desc: >-\n      O arquivo <1>config.yaml</1> já existe. Se você precisa redefinir algum dos itens de configuração deste arquivo, apague-o primeiro.\n    install_now: Você pode tentar <1>instalando agora</1>.\n    installed: Já instalado\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear a sua old database tables first.\n    db_failed: Falha ao conectar-se ao banco de dados\n    db_failed_desc: >-\n      This either means that the database information in a sua <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean a sua host's database server is down.\n  counts:\n    views: visualizações\n    votes: votos\n    answers: respostas\n    accepted: Aceito\n  page_error:\n    http_error: Erro HTTP {{ code }}\n    desc_403: Você não possui permissão para acessar esta página.\n    desc_404: Infelizmente esta página não existe.\n    desc_50X: Houve um erro no servidor e não foi possível completar a sua requisição.\n    back_home: Voltar para a página inicial\n  page_maintenance:\n    desc: \"Estamos em manutenção, voltaremos em breve.\"\n  nav_menus:\n    dashboard: Painel\n    contents: Conteúdos\n    questions: Perguntas\n    answers: Respostas\n    users: Usuários\n    badges: Emblemas\n    flags: Marcadores\n    settings: Configurações\n    general: Geral\n    interface: Interface\n    smtp: SMTP\n    branding: Marca\n    legal: Informação legal\n    write: Escrever\n    terms: Terms\n    tos: Termos de Serviços\n    privacy: Privacidade\n    seo: SEO\n    customize: Personalização\n    themes: Temas\n    login: Entrar\n    privileges: Privilégios\n    plugins: Extensões\n    installed_plugins: Plugins instalados\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Bem vindo(a) ao {{site_name}}\n  user_center:\n    login: Entrar\n    qrcode_login_tip: Por favor, utilize {{ agentName }} para escanear o QR code para entrar.\n    login_failed_email_tip: Falha ao entrar, por favor, permita que este aplicativo acesse a informação do seu e-mail antes de tentar novamente.\n  badges:\n    modal:\n      title: Parabéns\n      content: Você ganhou um novo emblema.\n      close: Fechar\n      confirm: Ver emblemas\n    title: Emblemas\n    awarded: Premiado\n    earned_×: Ganhou ×{{ number }}\n    ×_awarded: \"{{ number }} premiado\"\n    can_earn_multiple: Você pode ganhar isto várias vezes.\n    earned: Ganhou\n  admin:\n    admin_header:\n      title: Administrador\n    dashboard:\n      title: Painel\n      welcome: Bem-vindo ao Admin!\n      site_statistics: Estatísticas do site\n      questions: \"Perguntas:\"\n      resolved: \"Resolvido:\"\n      unanswered: \"Não Respondido:\"\n      answers: \"Respostas:\"\n      comments: \"Comentários:\"\n      votes: \"Votos:\"\n      users: \"Usuários:\"\n      flags: \"Marcadores:\"\n      reviews: \"Revisão:\"\n      site_health: Saúde do site\n      version: \"Versão:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload da pasta:\"\n      run_mode: \"Mode de execução:\"\n      private: Privado\n      public: Público\n      smtp: \"SMTP:\"\n      timezone: \"Fuso horário:\"\n      system_info: Informação do sistema\n      go_version: \"Versão do Go:\"\n      database: \"Banco de dados:\"\n      database_size: \"Tamanho do banco de dados:\"\n      storage_used: \"Armazenamento usado:\"\n      uptime: \"Tempo de atividade:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contato\n      forum: Fórum\n      documents: Documentos\n      feedback: Opinião\n      support: Supporte\n      review: Revisar\n      config: Configurações\n      update_to: Atualizar ao\n      latest: Ultimo\n      check_failed: Falha na verificação\n      \"yes\": \"Sim\"\n      \"no\": \"Não\"\n      not_allowed: Não permitido\n      allowed: Permitido\n      enabled: Ativo\n      disabled: Disponível\n      writable: Possível escrever\n      not_writable: Não é possível escrever\n    flags:\n      title: Marcadores\n      pending: Pendente\n      completed: Completo\n      flagged: Marcado\n      flagged_type: '{{ type }} sinalizado'\n      created: Criado\n      action: Ação\n      review: Revisar\n    user_role_modal:\n      title: Altere a função do usuário para...\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    new_password_modal:\n      title: Criar nova senha\n      form:\n        fields:\n          password:\n            label: Senha\n            text: O usuário será desconectado e precisar entrar novamente.\n            msg: A senha precisa ter no mínimo 8-32 caracteres.\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    edit_profile_modal:\n      title: Editar profile\n      form:\n        fields:\n          display_name:\n            label: Nome no display\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Nome do usuário\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Correio eletrônico\n            msg_invalid: Correio eletrônico invalido.\n      edit_success: Editado com sucesso\n      btn_cancel: Cancelar\n      btn_submit: Submeter\n    user_modal:\n      title: Adicionar novo usuário\n      form:\n        fields:\n          users:\n            label: Adicionar usuários em massa\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separe \"nome, e-mail, senha\" com vírgulas. Um usuário por linha.\n            msg: \"Por favor insira o e-mail do usuário, um por linha.\"\n          display_name:\n            label: Nome de exibição\n            msg: O nome de exibição deve ter entre 2 e 30 caracteres.\n          email:\n            label: E-mail\n            msg: E-mail inválido.\n          password:\n            label: Senha\n            msg: A senha precisa ter no mínimo 8-32 caracteres.\n      btn_cancel: Cancelar\n      btn_submit: Enviar\n    users:\n      title: Usuários\n      name: Nome\n      email: E-mail\n      reputation: Reputação\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Estado\n      role: Função\n      action: Ação\n      change: Mudar\n      all: Todos\n      staff: Funcionários\n      more: Mais\n      inactive: Inativo\n      suspended: Suspenso\n      deleted: Removido\n      normal: Normal\n      Moderator: Moderador\n      Admin: Administrador\n      User: Usuário\n      filter:\n        placeholder: \"Filtrar por nome, user:id\"\n      set_new_password: Configurar nova senha\n      edit_profile: Editar profile\n      change_status: Mudar status\n      change_role: Mudar função\n      show_logs: Mostrar registros\n      add_user: Adicionar usuário\n      deactivate_user:\n        title: Desativar usuários\n        content: Um usuário inativo deve revalidar seu e-mail.\n      delete_user:\n        title: Remover este usuário\n        content: Tem certeza de que deseja excluir este usuário? Isso é permanente!\n        remove: Remover o conteúdo dele\n        label: Remover todas as perguntas, respostas, comentários etc.\n        text: Não marque isso se deseja excluir apenas a conta do usuário.\n      suspend_user:\n        title: Suspender este usuário\n        content: Um usuário suspenso não pode fazer login.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Perguntas\n      unlisted: Não-listado\n      post: Publicação\n      votes: Votos\n      answers: Respostas\n      created: Criado\n      status: Estado\n      action: Ação\n      change: Mudar\n      pending: Pendente\n      filter:\n        placeholder: \"Filtrar por título, question:id\"\n    answers:\n      page_title: Respostas\n      post: Publicação\n      votes: Votos\n      created: Criado\n      status: Estado\n      action: Ação\n      change: Mudar\n      filter:\n        placeholder: \"Filtrar por título, answer:id\"\n    general:\n      page_title: Geral\n      name:\n        label: Site Nome\n        msg: Site name não pode ser vazio.\n        text: \"O nome deste site, conforme usado na tag de título.\"\n      site_url:\n        label: URL do Site\n        msg: Site url não pode ser vazio.\n        validate: Por favor digite uma URL válida.\n        text: O endereço do seu site.\n      short_desc:\n        label: Breve Descrição do site (opcional)\n        msg: Breve Descrição do site não pode ser vazio.\n        text: \"Breve descrição, conforme usado na tag de título na página inicial.\"\n      desc:\n        label: Site Descrição (opcional)\n        msg: Descrição do site não pode ser vazio.\n        text: \"Descreva este site em uma única sentença, conforme usado na meta tag de descrição.\"\n      contact_email:\n        label: E-mail para contato\n        msg: E-mail par contato não pode ser vazio.\n        validate: E-mail par contato não é válido.\n        text: Endereço de e-mail do principal contato responsável por este site.\n      check_update:\n        label: Atualizações de software\n        text: Verificar se há atualizações automaticamente\n    interface:\n      page_title: Interface\n      language:\n        label: Idioma da interface\n        msg: Idioma da Interface não pode ser vazio.\n        text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página.\n      time_zone:\n        label: Fuso horário\n        msg: Fuso horário não pode ser vazio.\n        text: Escolha a cidade no mesmo fuso horário que você.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: E-mail de origem\n        msg: E-mail de origem não pode ser vazio.\n        text: O endereço de e-mail de onde os e-mails são enviados.\n      from_name:\n        label: Nome de origem\n        msg: Nome de origem não pode ser vazio.\n        text: O nome de onde os e-mails são enviados.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host não pode ser vazio.\n        text: O seu servidor de e-mails.\n      encryption:\n        label: Criptografia\n        msg: Criptografia não pode ser vazio.\n        text: Para a maioria dos servidores SSL é a opção recomendada.\n        ssl: SSL\n        tls: TLS\n        none: Nenhum\n      smtp_port:\n        label: SMTP Port\n        msg: Porta SMTP deve ser o número 1 ~ 65535.\n        text: The port to a sua mail server.\n      smtp_username:\n        label: SMTP Nome de usuário\n        msg: SMTP username não pode ser vazio.\n      smtp_password:\n        label: SMTP Senha\n        msg: SMTP password não pode ser vazio.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Forneça o endereço de e-mail que irá receber envios de teste.\n        msg: O e-mail de teste é inválido\n      smtp_authentication:\n        label: Habilitar autenticação\n        title: Autenticação SMTP\n        msg: Autenticação SMTP não pode ser vazio.\n        \"yes\": \"Sim\"\n        \"no\": \"Não\"\n    branding:\n      page_title: Marca\n      logo:\n        label: Logo (opcional)\n        msg: Logo não pode ser vazio.\n        text: The logo image at the top left of a sua site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.\n      mobile_logo:\n        label: Mobile Logo (opcional)\n        text: The logo used on mobile version of a sua site. Use a wide rectangular image with a height of 56. If left blank, the image from the \"logo\" setting will be used.\n      square_icon:\n        label: Square Icon (opcional)\n        msg: Square icon não pode ser vazio.\n        text: Imagem used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (opcional)\n        text: A favicon for a sua site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, \"square icon\" will be used.\n    legal:\n      page_title: Informação legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Escrever resposta\n        label: Each user can only write one answer for each question\n        text: \"Desative para permitir que os usuários escrevam várias respostas para a mesma pergunta, o que pode fazer com que as respostas fiquem menos focadas.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend Marcadores\n        text: \"Os marcadores recomendados serão exibidos na lista dropdown por padrão.\"\n        msg:\n          contain_reserved: \"tags recomendadas não podem conter tags reservadas\"\n      required_tag:\n        title: Definir tags necessárias\n        label: Definir \"Tags recomendadas\" como tags necessárias\n        text: \"Every new question must have ao menos one recommend tag.\"\n      reserved_tags:\n        label: Reserved Marcadores\n        text: \"Tags reservadas só podem ser usadas pelo moderador.\"\n      image_size:\n        label: Tamanho máximo da imagem (MB)\n        text: \"O tamanho máximo para upload de imagem.\"\n      attachment_size:\n        label: Tamanho máximo do anexo (MB)\n        text: \"O tamanho máximo para o carregamento de arquivos anexados.\"\n      image_megapixels:\n        label: Máximo megapíxels da imagem\n        text: \"Número máximo de megapixels permitido para uma imagem.\"\n      image_extensions:\n        label: Extensões de imagens autorizadas\n        text: \"Uma lista de extensões de arquivo permitidas para exibição de imagens, separadas por vírgula.\"\n      attachment_extensions:\n        label: Extensões autorizadas para anexos\n        text: \"Uma lista de extensões de arquivo permitidas para carregamento, separar por vírgula. AVISO: permitir o carregamento pode causar problemas de segurança.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Link permanente\n        text: Custom URL structures can improve the usability, and forward-compatibility of a sua links.\n      robots:\n        label: robos.txt\n        text: Isto irá substituir permanentemente quaisquer configurações do site relacionadas.\n    themes:\n      page_title: Temas\n      themes:\n        label: Temas\n        text: Selecionar um tema existente.\n      color_scheme:\n        label: Esquema de cores\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Cor primária\n        text: Modifica as cores usadas por seus temas\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS e HTML\n      custom_css:\n        label: CSS Personalizado\n        text: >\n\n      head:\n        label: Cabeçalho\n        text: >\n\n      header:\n        label: Cabeçalho\n        text: >\n\n      footer:\n        label: Rodapé\n        text: Isto será inserido antes de &lt;/body>.\n      sidebar:\n        label: Barra lateral\n        text: Isto irá inserir na barra lateral.\n    login:\n      page_title: Entrar\n      membership:\n        title: Afiliação\n        label: Permitir novas inscrições\n        text: Desligue para impedir que alguém crie uma nova conta.\n      email_registration:\n        title: Registrar e-mail\n        label: Permitir registro utilizando e-mail\n        text: Desative para impedir que qualquer pessoa crie uma nova conta por e-mail.\n      allowed_email_domains:\n        title: Domínios de e-mail permitidos\n        text: Domínios de e-mail com os quais os usuários devem registrar contas. Um domínio por linha. Ignorado quando vazio.\n      private:\n        title: Privado\n        label: Login requirido\n        text: Somente usuários conectados podem acessar esta comunidade.\n      password_login:\n        title: Login com senha\n        label: Permitir login por e-mail e senha\n        text: \"AVISO: Se desativar, você pode ser incapaz de efetuar login se você não tiver configurado anteriormente outro método de login.\"\n    installed_plugins:\n      title: Extensões instaladas\n      plugin_link: Plugins ampliam e expandem a funcionalidade. Você pode encontrar plugins no <1>Repositório de Plugins</1>.\n      filter:\n        all: Todos\n        active: Ativo\n        inactive: Inativo\n        outdated: Desactualizado\n      plugins:\n        label: Extensões\n        text: Selecionar uma extensão existente.\n      name: Nome\n      version: Versão\n      status: Estado\n      action: Ação\n      deactivate: Desativar\n      activate: Ativado\n      settings: Configurações\n    settings_users:\n      title: Usuários\n      avatar:\n        label: Avatar padrão\n        text: Para usuários sem um avatar personalizado próprio.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL da API do provedor Gravatar ignorado quando vazio.\n      profile_editable:\n        title: Perfil editável\n      allow_update_display_name:\n        label: Permitir que os usuários mudem seus nomes de exibição\n      allow_update_username:\n        label: Permitem que os usuário mudem seus nomes de usuário\n      allow_update_avatar:\n        label: Permitem que os usuário mudem a sua imagem de perfil\n      allow_update_bio:\n        label: Permitir que os usuários mudem suas descrições\n      allow_update_website:\n        label: Permitir que os usuários mudem seus web-sites\n      allow_update_location:\n        label: Permitir que usuários alterem suas localizações\n    privilege:\n      title: Privilégios\n      level:\n        label: Nível de reputação necessário\n        text: Escolha a reputação necessária para os privilégios\n      msg:\n        should_be_number: o valor de entrada deve ser número\n        number_larger_1: número deve ser igual ou maior que 1\n    badges:\n      action: Ação\n      active: Ativo\n      activate: Ativado\n      all: Todos\n      awards: Prêmios\n      deactivate: Desativar\n      filter:\n        placeholder: Filtrar por nome, badge:id\n      group: Grupo\n      inactive: Inativo\n      name: Nome\n      show_logs: Mostrar registros\n      status: Status\n      title: Emblemas\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (opcional)\n    empty: não pode ser vazio\n    invalid: é inválido\n    btn_submit: Salvar\n    not_found_props: \"Propriedade requerida {{ key }} não encontrada.\"\n    select: Selecionar\n  page_review:\n    review: Revisar\n    proposed: proposto\n    question_edit: Editar pergunta\n    answer_edit: Editar resposta\n    tag_edit: Editar marcador\n    edit_summary: Editar descrição\n    edit_question: Editar pergunta\n    edit_answer: Editar resposta\n    edit_tag: Editar marcador\n    empty: Nenhuma tarefa de revisão restante.\n    approve_revision_tip: Você aprova esta revisão?\n    approve_flag_tip: Você aprova esta sinalização?\n    approve_post_tip: Você aprova esta publicação?\n    approve_user_tip: Você aprova este usuário?\n    suggest_edits: Edições sugeridas\n    flag_post: Post sinalizado\n    flag_user: Sinalizar usuário\n    queued_post: Publicação na fila\n    queued_user: Usuário na fila\n    filter_label: Tipo\n    reputation: reputação\n    flag_post_type: Sinalizou esta publicação como {{ type }}.\n    flag_user_type: Sinalizou este usuário como {{ type }}.\n    edit_post: Editar publicação\n    list_post: Listar postagem\n    unlist_post: Remover postagem da lista\n  timeline:\n    undeleted: não removido\n    deleted: removido\n    downvote: voto negativo\n    upvote: voto positivo\n    accept: aceito\n    cancelled: cancelado\n    commented: comentado\n    rollback: reversão\n    edited: editado\n    answered: respondido\n    asked: perguntado\n    closed: fechado\n    reopened: reaberto\n    created: criado\n    pin: fixado\n    unpin: desafixado\n    show: listadas\n    hide: não listado\n    title: \"Histórico para\"\n    tag_title: \"Título para\"\n    show_votes: \"Mostrar Votos\"\n    n_or_a: Não aplicável\n    title_for_question: \"Título para\"\n    title_for_answer: \"Título para resposta {{ title }} por {{ author }}\"\n    title_for_tag: \"Título para marcador\"\n    datetime: Data e hora\n    type: Tipo\n    by: Por\n    comment: Comentário\n    no_data: \"Não conseguimos encontrar nada.\"\n  users:\n    title: Usuários\n    users_with_the_most_reputation: Usuários com maior pontuação\n    users_with_the_most_vote: Usuários que mais votaram\n    staffs: Nossos colaboradores\n    reputation: reputação\n    votes: votos\n  prompt:\n    leave_page: Tem a certeza que quer sair desta página?\n    changes_not_save: Suas alterações não podem ser salvas.\n  draft:\n    discard_confirm: Tem certeza que deseja descartar o rascunho?\n  messages:\n    post_deleted: Esta publicação foi removida.\n    post_cancel_deleted: Esta postagem foi restaurada.\n    post_pin: Esta publicação foi fixada.\n    post_unpin: Esta postagem foi desafixada.\n    post_hide_list: Esta postagem foi ocultada da lista.\n    post_show_list: Esta postagem foi exibida à lista.\n    post_reopen: Esta publicação foi re-aberta.\n    post_list: Esta postagem foi listada.\n    post_unlist: Esta publicação foi removida da lista.\n    post_pending: A sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada.\n    post_closed: Esta postagem foi fechada.\n    answer_deleted: Esta resposta foi excluída.\n    answer_cancel_deleted: Esta resposta foi restaurada.\n    change_user_role: O papel deste usuário foi alterado.\n    user_inactive: Este usuário já está inativo.\n    user_normal: Este usuário já está normal.\n    user_suspended: Este usuário foi suspenso.\n    user_deleted: Este usuário foi removido.\n    user_added: User has been added successfully.\n    badge_activated: Este emblema foi ativado.\n    badge_inactivated: Este emblema foi desativado.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/ro_RO.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Succes.\n    unknown:\n      other: Eroare necunoscută.\n    request_format_error:\n      other: Formatul cererii nu este valid.\n    unauthorized_error:\n      other: Neautorizat.\n    database_error:\n      other: Eroare la serverul de date.\n    forbidden_error:\n      other: Interzis.\n    duplicate_request_error:\n      other: Trimitere dublă.\n  action:\n    report:\n      other: Steag\n    edit:\n      other: Editează\n    delete:\n      other: Ștergere\n    close:\n      other: Închide\n    reopen:\n      other: Redeschidere\n    forbidden_error:\n      other: Interzis.\n    pin:\n      other: Fixează\n    hide:\n      other: Dezlistare\n    unpin:\n      other: Anulați fixarea\n    show:\n      other: Listă\n    invite_someone_to_answer:\n      other: Editează\n    undelete:\n      other: Restabilește\n    merge:\n      other: Îmbinare\n  role:\n    name:\n      user:\n        other: Utilizator\n      admin:\n        other: Administrator\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Implicit fără acces special.\n      admin:\n        other: Ai puterea deplină de a accesa site-ul.\n      moderator:\n        other: Are acces la toate postările cu excepţia setărilor administratorului.\n  privilege:\n    level_1:\n      description:\n        other: Nivel 1 (mai puțină reputație pentru echipa privată, grup)\n    level_2:\n      description:\n        other: Nivelul 2 (reputație scăzută necesară pentru comunitatea de pornire)\n    level_3:\n      description:\n        other: Nivelul 3 (reputație ridicată necesară pentru comunitatea mature)\n    level_custom:\n      description:\n        other: Nivel personalizat\n    rank_question_add_label:\n      other: Întreabă ceva\n    rank_answer_add_label:\n      other: Scrie răspunsul\n    rank_comment_add_label:\n      other: Scrie comentariu\n    rank_report_add_label:\n      other: Steag\n    rank_comment_vote_up_label:\n      other: Votează comentariul\n    rank_link_url_limit_label:\n      other: Postează mai mult de 2 link-uri simultan\n    rank_question_vote_up_label:\n      other: Votează întrebarea\n    rank_answer_vote_up_label:\n      other: Votează răspunsul\n    rank_question_vote_down_label:\n      other: Votează întrebarea ca negativa\n    rank_answer_vote_down_label:\n      other: Voteaza răspunsul ca negativ\n    rank_invite_someone_to_answer_label:\n      other: Invită pe cineva să răspundă\n    rank_tag_add_label:\n      other: Creează o etichetă nouă\n    rank_tag_edit_label:\n      other: Editați descrierea etichetei (este nevoie de revizuire)\n    rank_question_edit_label:\n      other: Editați altă întrebare (este nevoie de revizuire)\n    rank_answer_edit_label:\n      other: Editați altă întrebare (este nevoie de revizuire)\n    rank_question_edit_without_review_label:\n      other: Editează întrebarea celuilalt fără revizuire\n    rank_answer_edit_without_review_label:\n      other: Editează întrebarea celuilalt fără revizuire\n    rank_question_audit_label:\n      other: Revizuiește editarea întrebărilor\n    rank_answer_audit_label:\n      other: Revizuiește editările răspunsurilor\n    rank_tag_audit_label:\n      other: Revizuiește editarea etichetelor\n    rank_tag_edit_without_review_label:\n      other: Editează descrierea etichetei fără revizuire\n    rank_tag_synonym_label:\n      other: Gestionează sinonimele etichetelor\n  email:\n    other: E-mail\n  e_mail:\n    other: E-mail\n  password:\n    other: Parolă\n  pass:\n    other: Parolă\n  old_pass:\n    other: Parolă actuală\n  original_text:\n    other: Acest articol\n  email_or_password_wrong_error:\n    other: E-mailul și parola nu se potrivesc.\n  error:\n    common:\n      invalid_url:\n        other: URL invalid.\n      status_invalid:\n        other: Stare nevalidă.\n    password:\n      space_invalid:\n        other: Parola nu poate conține spații.\n    admin:\n      cannot_update_their_password:\n        other: Nu vă puteți modifica parola.\n      cannot_edit_their_profile:\n        other: Nu vă puteți modifica profilul.\n      cannot_modify_self_status:\n        other: Nu vă puteți modifica starea.\n      email_or_password_wrong:\n        other: E-mailul și parola nu se potrivesc.\n    answer:\n      not_found:\n        other: Răspunsul nu a fost găsit.\n      cannot_deleted:\n        other: Nu există permisiunea de ștergere.\n      cannot_update:\n        other: Nu există permisiunea de ștergere.\n      question_closed_cannot_add:\n        other: Întrebările sunt închise şi nu pot fi adăugate.\n      content_cannot_empty:\n        other: Conținutul răspunsului nu poate fi gol.\n    comment:\n      edit_without_permission:\n        other: Comentariul nu poate fi editat.\n      not_found:\n        other: Comentariul nu a fost găsit.\n      cannot_edit_after_deadline:\n        other: Comentariul a durat prea mult pentru a fi modificat.\n      content_cannot_empty:\n        other: Conținutul comentariului nu poate fi gol.\n    email:\n      duplicate:\n        other: Email-ul există deja.\n      need_to_be_verified:\n        other: E-mailul trebuie verificat.\n      verify_url_expired:\n        other: Adresa de e-mail verificată a expirat, vă rugăm să retrimiteți e-mailul.\n      illegal_email_domain_error:\n        other: E-mailul nu este permis din acel domeniu de e-mail. Vă rugăm să folosiți altul.\n    lang:\n      not_found:\n        other: Fișierul de limbă nu a fost găsit.\n    object:\n      captcha_verification_failed:\n        other: Captcha este greșit.\n      disallow_follow:\n        other: Nu vă este permis să urmăriți.\n      disallow_vote:\n        other: Nu ai permisiunea de a vota.\n      disallow_vote_your_self:\n        other: Nu poți vota pentru propria ta postare.\n      not_found:\n        other: Obiectul nu a fost găsit.\n      verification_failed:\n        other: Verificarea a eșuat.\n      email_or_password_incorrect:\n        other: E-mailul și parola nu se potrivesc.\n      old_password_verification_failed:\n        other: Verificarea parolei vechi a eșuat\n      new_password_same_as_previous_setting:\n        other: Noua parolă este identică cu cea anterioară.\n      already_deleted:\n        other: Acest articol a fost șters.\n    meta:\n      object_not_found:\n        other: Nu s-a găsit obiectul Meta\n    question:\n      already_deleted:\n        other: Această postare a fost ștearsă.\n      under_review:\n        other: Articolul tău este în așteptare. Acesta va fi vizibil după ce a fost aprobat.\n      not_found:\n        other: Întrebarea nu a fost găsită.\n      cannot_deleted:\n        other: Nu există permisiunea de ștergere.\n      cannot_close:\n        other: Nu există permisiunea de a închide.\n      cannot_update:\n        other: Nu aveți permisiunea de a actualiza.\n      content_cannot_empty:\n        other: Conținutul nu poate fi gol.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rangul de reputaţie nu îndeplineşte condiţia.\n      vote_fail_to_meet_the_condition:\n        other: Mulțumim pentru feedback. Aveți nevoie cel puțin de reputația {{.Rank}} pentru a vota.\n      no_enough_rank_to_operate:\n        other: Aveți nevoie cel puțin de reputația {{.Rank}} pentru a face asta.\n    report:\n      handle_failed:\n        other: Procesarea raportării a eșuat.\n      not_found:\n        other: Raportul nu a fost găsit.\n    tag:\n      already_exist:\n        other: Eticheta există deja.\n      not_found:\n        other: Eticheta nu a fost găsită.\n      recommend_tag_not_found:\n        other: Eticheta recomandată nu există.\n      recommend_tag_enter:\n        other: Te rugăm să introduci cel puțin o etichetă necesară.\n      not_contain_synonym_tags:\n        other: Nu trebuie să conțină etichete sinonime.\n      cannot_update:\n        other: Nu aveți permisiunea de a actualiza.\n      is_used_cannot_delete:\n        other: Nu puteți șterge o etichetă care este în uz.\n      cannot_set_synonym_as_itself:\n        other: Nu se poate seta sinonimul etichetei curente ca atare.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Numele nu poate fi o adresă de e-mail.\n    theme:\n      not_found:\n        other: Tema nu a fost găsită.\n    revision:\n      review_underway:\n        other: Nu se poate edita momentan, există o versiune în coada de revizuire.\n      no_permission:\n        other: Nu ai permisiunea de a revizui.\n    user:\n      external_login_missing_user_id:\n        other: Platforma terță nu oferă un Id de utilizator unic, deci nu vă puteți autentifica, contactați administratorul site-ului.\n      external_login_unbinding_forbidden:\n        other: Vă rugăm să setaţi o parolă de conectare pentru contul dumneavoastră înainte de a elimina această autentificare.\n      email_or_password_wrong:\n        other:\n          other: E-mailul și parola nu se potrivesc.\n      not_found:\n        other: Utilizatorul nu a fost găsit.\n      suspended:\n        other: Utilizatorul a fost suspendat.\n      username_invalid:\n        other: Numele de utilizator nu este valid.\n      username_duplicate:\n        other: Numele de utilizator este deja luat.\n      set_avatar:\n        other: Setarea avatarului a eșuat.\n      cannot_update_your_role:\n        other: Nu vă puteți modifica rolul.\n      not_allowed_registration:\n        other: În prezent, site-ul nu este deschis pentru înregistrare.\n      not_allowed_login_via_password:\n        other: În prezent, site-ul nu este permis să se autentifice prin parolă.\n      access_denied:\n        other: Acces Blocat\n      page_access_denied:\n        other: Nu aveți acces la această pauză.\n      add_bulk_users_format_error:\n        other: \"{{.Field}} format lângă '{{.Content}}' la linia {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Numărul de utilizatori pe care îi adăugați odată trebuie să fie în intervalul 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Citirea configurației a eșuat\n    database:\n      connection_failed:\n        other: Conexiunea la baza de date a eșuat\n      create_table_failed:\n        other: Crearea tabelului a eșuat\n    install:\n      create_config_failed:\n        other: Nu se poate crea fișierul config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Format de fișier incompatibil.\n    site_info:\n      config_not_found:\n        other: Configurarea site-ului nu a fost găsită.\n    badge:\n      object_not_found:\n        other: Nu s-a găsit obiectul Insignă\n  reason:\n    spam:\n      name:\n        other: nedorite\n      desc:\n        other: Acest post este o reclamă sau un vandalism. Nu este util sau relevant pentru subiectul actual.\n    rude_or_abusive:\n      name:\n        other: nepoliticos sau abuziv\n      desc:\n        other: \"O persoană rezonabilă ar considera acest conținut nepotrivit pentru un discurs respectuos.\"\n    a_duplicate:\n      name:\n        other: un duplicat\n      desc:\n        other: Această întrebare a fost adresată înainte şi are deja un răspuns.\n      placeholder:\n        other: Introduceți link-ul de întrebare existent\n    not_a_answer:\n      name:\n        other: nu este un răspuns\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: nu mai este necesar\n      desc:\n        other: Acest comentariu este învechit, conversaţional sau nu are relevanţă pentru această postare.\n    something:\n      name:\n        other: altceva\n      desc:\n        other: Acest post necesită atenție din partea personalului, din alt motiv nemenționat mai sus.\n      placeholder:\n        other: Spune-ne ce anume vă îngrijorează\n    community_specific:\n      name:\n        other: un motiv specific comunității\n      desc:\n        other: Această întrebare nu corespunde cu ghidul comunității.\n    not_clarity:\n      name:\n        other: are nevoie de detalii sau de claritate\n      desc:\n        other: Această întrebare include în prezent mai multe întrebări. Ar trebui să se concentreze asupra unei singure probleme.\n    looks_ok:\n      name:\n        other: arată OK\n      desc:\n        other: Această postare este bună și nu este de slabă calitate.\n    needs_edit:\n      name:\n        other: are nevoie de editare și am făcut-o\n      desc:\n        other: Îmbunătățește și corectează problemele cu această postare.\n    needs_close:\n      name:\n        other: necesită închidere\n      desc:\n        other: La o întrebare închisă nu poți răspunde, dar poți totuși să o editezi, să o votezi și să o comentezi.\n    needs_delete:\n      name:\n        other: necesită ștergere\n      desc:\n        other: Această postare va fi ștearsă.\n  question:\n    close:\n      duplicate:\n        name:\n          other: nedorite\n        desc:\n          other: Această întrebare a fost adresată înainte şi are deja un răspuns.\n      guideline:\n        name:\n          other: un motiv specific comunității\n        desc:\n          other: Această întrebare nu corespunde cu ghidul comunității.\n      multiple:\n        name:\n          other: necesită detalii sau claritate\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: altceva\n        desc:\n          other: Acest post necesită un alt motiv care nu este listat mai sus.\n    operation_type:\n      asked:\n        other: întrebat\n      answered:\n        other: răspunse\n      modified:\n        other: modificat\n    deleted_title:\n      other: Întrebare ștearsă\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: întrebarea actualizată\n      answer_the_question:\n        other: întrebare răspunsă\n      update_answer:\n        other: răspuns actualizat\n      accept_answer:\n        other: răspuns acceptat\n      comment_question:\n        other: întrebare comentată\n      comment_answer:\n        other: răspuns comentat\n      reply_to_you:\n        other: ți-a răspuns\n      mention_you:\n        other: te-a menționat\n      your_question_is_closed:\n        other: Întrebarea dumneavoastră a fost închisă\n      your_question_was_deleted:\n        other: Întâlnirea dumneavoastră a fost ştearsă\n      your_answer_was_deleted:\n        other: Răspunsul dumneavoastră a fost șters\n      your_comment_was_deleted:\n        other: Contul dumneavoastră a fost șters\n      up_voted_question:\n        other: votează întrebarea\n      down_voted_question:\n        other: votează întrebarea negativ\n      up_voted_answer:\n        other: votează răspunsul\n      down_voted_answer:\n        other: răspuns negativ\n      up_voted_comment:\n        other: votează comentariul\n      invited_you_to_answer:\n        other: te-a invitat să răspunzi\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirmați noua dvs. adresă de e-mail\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} vă invită să răspundeți\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Întrebare nouă: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Resetare parolă\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirmă noul tău cont\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test de e-mail\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: votat\n    upvoted:\n      other: vot pozitiv\n    downvote:\n      other: vot negativ\n    downvoted:\n      other: vot negativ\n    accept:\n      other: acceptat\n    accepted:\n      other: acceptat\n    edit:\n      other: editează\n  review:\n    queued_post:\n      other: Posturi în așteptare\n    flagged_post:\n      other: Postare marcată\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Contribuții restante în prima lor lună.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Distribuire grozavă\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Cum se formatează\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Înapoi\n    next: Înainte\n  page_title:\n    question: Întrebare\n    questions: Întrebări\n    tag: Etichetă\n    tags: Etichete\n    tag_wiki: etichetă wiki\n    create_tag: Creați etichetă\n    edit_tag: Modificați eticheta\n    ask_a_question: Create Question\n    edit_question: Editați întrebarea\n    edit_answer: Editaţi răspunsul\n    search: Caută\n    posts_containing: Posturi care conțin\n    settings: Setări\n    notifications: Notificări\n    login: Conectează-te\n    sign_up: Înregistrează-te\n    account_recovery: Recuperarea contului\n    account_activation: Activare cont\n    confirm_email: Confirmare e-mail\n    account_suspended: Cont suspendat\n    admin: Administrator\n    change_email: Modifică E-mail\n    install: Instalează Answer\n    upgrade: Actualizare Answer\n    maintenance: Mentenanță website\n    users: Utilizatori\n    oauth_callback: Se procesează\n    http_404: Eroare HTTP 404\n    http_50X: Eroare HTTP 500\n    http_403: Eroare HTTP 403\n    logout: Deconectare\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notificări\n    inbox: Mesaje primite\n    achievement: Realizări\n    new_alerts: Alerte noi\n    all_read: Marchează totul ca fiind citit\n    show_more: Arată mai mult\n    someone: Cineva\n    inbox_type:\n      all: Toate\n      posts: Postări\n      invites: Invitați\n      votes: Voturi\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Contul dumneavoastră a fost suspendat\n    until_time: \"Contul dumneavoastră a fost suspendat până la {{ time }}.\"\n    forever: Acest utilizator a fost suspendat pentru totdeauna.\n    end: Această întrebare nu corespunde cu ghidul comunității.\n    contact_us: Contactați-ne\n  editor:\n    blockquote:\n      text: Citat\n    bold:\n      text: Bolt\n    chart:\n      text: Diagramă\n      flow_chart: Diagrama fluxului\n      sequence_diagram: Diagrama secvenței\n      class_diagram: Diagrama clasei\n      state_diagram: Diagrama stării\n      entity_relationship_diagram: Diagrama relației entității\n      user_defined_diagram: Diagramă definită de utilizator\n      gantt_chart: Grafic Gantt\n      pie_chart: Grafic circular\n    code:\n      text: Exemplu de cod\n      add_code: Adaugă exemplu de cod\n      form:\n        fields:\n          code:\n            label: Cod\n            msg:\n              empty: Corpul mesajului trebuie să conțină text.\n          language:\n            label: Limbă\n            placeholder: Detectare automată\n      btn_cancel: Anulați\n      btn_confirm: Adaugă\n    formula:\n      text: Formulă\n      options:\n        inline: Formula inline\n        block: Formula blocului\n    heading:\n      text: Titlu\n      options:\n        h1: Titlu 1\n        h2: Titlu 2\n        h3: Titlu 3\n        h4: Titlu 4\n        h5: Titlu 5\n        h6: Titlu 6\n    help:\n      text: Ajutor\n    hr:\n      text: Linie orizontală\n    image:\n      text: Imagine\n      add_image: Adaugă imagine\n      tab_image: Incarca poza\n      form_image:\n        fields:\n          file:\n            label: Fișier imagine\n            btn: Selectați imaginea\n            msg:\n              empty: Fișierul nu poate fi gol.\n              only_image: Sunt permise doar fișierele imagine.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Descriere\n      tab_url: URL-ul imaginii\n      form_url:\n        fields:\n          url:\n            label: URL-ul imaginii\n            msg:\n              empty: URL-ul imaginii nu poate fi gol.\n          name:\n            label: Descriere\n      btn_cancel: Anulați\n      btn_confirm: Adaugă\n      uploading: Se încarcă\n    indent:\n      text: Indentare\n    outdent:\n      text: Outdent\n    italic:\n      text: Accentuare\n    link:\n      text: Hyperlink\n      add_link: Adaugă hiperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL-ul nu poate fi gol.\n          name:\n            label: Descriere\n      btn_cancel: Anulează\n      btn_confirm: Adaugă\n    ordered_list:\n      text: Listă numerotată\n    unordered_list:\n      text: Listă cu marcatori\n    table:\n      text: Tabelă\n      heading: Titlu\n      cell: Celulă\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Închid această postare ca...\n    btn_cancel: Anulează\n    btn_submit: Trimiteți\n    remark:\n      empty: Nu poate fi lăsat necompletat.\n    msg:\n      empty: Te rugăm să selectezi un motiv.\n  report_modal:\n    flag_title: Fac un semnal de alarmă pentru a raporta acest post ca...\n    close_title: Închid această postare ca...\n    review_question_title: Revizuiește întrebarea\n    review_answer_title: Revizuiește răspunsul\n    review_comment_title: Revizuiește comentariul\n    btn_cancel: Anulează\n    btn_submit: Trimiteți\n    remark:\n      empty: Nu poate fi lăsat necompletat.\n    msg:\n      empty: Te rugăm să selectezi un motiv.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Creează o etichetă nouă\n    form:\n      fields:\n        display_name:\n          label: Nume afișat\n          msg:\n            empty: Numele afișat nu poate fi gol.\n            range: Nume afișat până la 35 de caractere.\n        slug_name:\n          label: Slug URL\n          desc: Slug-ul URL pana la 35 de caractere.\n          msg:\n            empty: Slug-ul URL nu poate fi gol.\n            range: Slug-ul URL pana la 35 de caractere.\n            character: URL-ul slug conţine un set de caractere nepermis.\n        desc:\n          label: Descriere\n        revision:\n          label: Versiunea\n        edit_summary:\n          label: Editează sumarul\n          placeholder: >-\n            Explicați pe scurt modificările (ortografie corectată, gramatică fixată, formatare îmbunătățită)\n    btn_cancel: Anulează\n    btn_submit: Trimiteți\n    btn_post: Postează o nouă etichetă\n  tag_info:\n    created_at: Creat\n    edited_at: Editat\n    history: Istoric\n    synonyms:\n      title: Sinonime\n      text: Următoarele etichete vor fi păstrate la\n      empty: Nu s-au găsit sinonime.\n      btn_add: Adaugă un sinonim\n      btn_edit: Editează\n      btn_save: Salvează\n    synonyms_text: Următoarele etichete vor rămâne la\n    delete:\n      title: Șterge această etichetă\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Sunteţi sigur că doriţi să ştergeţi?\n      close: Închide\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Editează eticheta\n    default_reason: Editare etichetă\n    default_first_reason: Adaugă etichetă\n    btn_save_edits: Salvați modificările\n    btn_cancel: Anulați\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, AAAA [at] HH:mm\"\n    now: acum\n    x_seconds_ago: \"acum {{count}} sec\"\n    x_minutes_ago: \"acum {{count}} min\"\n    x_hours_ago: \"acum {{count}} ore\"\n    hour: oră\n    day: zi\n    hours: ore\n    days: zile\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Adaugă comentariu\n    reply_to: Raspunde la\n    btn_reply: Răspunde\n    btn_edit: Editează\n    btn_delete: Ștergeți\n    btn_flag: Marcaj\n    btn_save_edits: Salvați modificările\n    btn_cancel: Anulați\n    show_more: \"{{count}} alte comentarii\"\n    tip_question: >-\n      Utilizați comentariile pentru a solicita mai multe informații sau pentru a sugera îmbunătățiri. Evitați răspunsul la întrebări în comentarii.\n    tip_answer: >-\n      Utilizați comentarii pentru a răspunde la alți utilizatori sau pentru a le notifica modificările. Dacă adăugați informații noi, editați postarea în loc să comentați.\n    tip_vote: Adaugă ceva util postării\n  edit_answer:\n    title: Editaţi răspunsul\n    default_reason: Editați răspunsul\n    default_first_reason: Adăugare răspuns\n    form:\n      fields:\n        revision:\n          label: Revizuire\n        answer:\n          label: Răspuns\n          feedback:\n            characters: conţinutul trebuie să aibă cel puţin 6 caractere.\n        edit_summary:\n          label: Editează sumarul\n          placeholder: >-\n            Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită)\n    btn_save_edits: Salvați modificările\n    btn_cancel: Anulează\n  tags:\n    title: Etichete\n    sort_buttons:\n      popular: Popular\n      name: Nume\n      newest: Cele mai noi\n    button_follow: Urmărește\n    button_following: Urmăriți\n    tag_label: întrebări\n    search_placeholder: Filtrare după numele etichetei\n    no_desc: Această echipă nu are o descriere.\n    more: Mai multe\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Editați întrebarea\n    default_reason: Editați întrebarea\n    default_first_reason: Create question\n    similar_questions: Întrebări similare\n    form:\n      fields:\n        revision:\n          label: Revizuire\n        title:\n          label: Titlu\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Titlul nu poate fi gol.\n            range: Titlu de până la 150 de caractere\n        body:\n          label: Corp\n          msg:\n            empty: Corpul mesajului trebuie să conțină text.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Etichete\n          msg:\n            empty: Etichetele nu pot fi goale.\n        answer:\n          label: Răspuns\n          msg:\n            empty: Răspunsul nu poate fi gol.\n        edit_summary:\n          label: Editează sumarul\n          placeholder: >-\n            Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită)\n    btn_post_question: Postează întrebarea ta\n    btn_save_edits: Salvați modificările\n    answer_question: Răspundeți la propria întrebare\n    post_question&answer: Postează-ți întrebarea și răspunsul\n  tag_selector:\n    add_btn: Adaugă etichetă\n    create_btn: Creează o etichetă nouă\n    search_tag: Căutare etichetă\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Nicio etichetă potrivită\n    tag_required_text: Etichetă necesară (cel puțin una)\n  header:\n    nav:\n      question: Întrebări\n      tag: Etichete\n      user: Utilizatori\n      badges: Badges\n      profile: Profil\n      setting: Setări\n      logout: Deconectaţi-vă\n      admin: Administrator\n      review: Recenzie\n      bookmark: Semne de carte\n      moderation: Moderare\n    search:\n      placeholder: Caută\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Schimbare\n    loading: încarcare...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Introdu textul de mai sus\n    msg:\n      empty: Captcha nu poate fi gol.\n  inactive:\n    first: >-\n      Ești aproape gata! Am trimis un e-mail de activare la <bold>{{mail}}</bold>. Te rugăm să urmezi instrucțiunile din e-mail pentru a-ți activa contul.\n    info: \"Dacă nu ajunge, verifică folderul Spam.\"\n    another: >-\n      Ți-am trimis un alt e-mail de activare la <bold>{{mail}}</bold>. Poate dura câteva minute până ajuns; asiguraţi-vă că verificaţi folderul Spam.\n    btn_name: Retrimitere link de activare\n    change_btn_name: Schimbați e-mailul\n    msg:\n      empty: Nu poate fi lăsat necompletat.\n    resend_email:\n      url_label: Sunteţi sigur că doriţi să retrimiteţi e-mailul de activare?\n      url_text: De asemenea, puteți da link-ul de activare de mai sus utilizatorului.\n  login:\n    login_to_continue: Conectează-te pentru a continua\n    info_sign: Nu ai un cont? Înregistrează-te\n    info_login: Ai deja un cont? <1>Autentifică-te</1>\n    agreements: Prin înregistrare, ești de acord cu <1>politica de confidențialitate</1> și <3>termenii și condițiile de utilizare</3>.\n    forgot_pass: Ai uitat parola?\n    name:\n      label: Nume\n      msg:\n        empty: Câmpul Nume trebuie completat.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-mail\n      msg:\n        empty: Câmpul e-mail nu poate fi gol.\n    password:\n      label: Parolă\n      msg:\n        empty: Parola nu poate fi goală.\n        different: Parolele introduse pe ambele părți sunt incompatibile\n  account_forgot:\n    page_title: Ati uitat parola\n    btn_name: Trimite-mi e-mail de recuperare\n    send_success: >-\n      Dacă un cont corespunde cu <strong>{{mail}}</strong>, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp.\n    email:\n      label: E-mail\n      msg:\n        empty: Câmpul e-mail nu poate fi gol.\n  change_email:\n    btn_cancel: Anulați\n    btn_update: Actualizare adresă de e-mail\n    send_success: >-\n      Dacă un cont corespunde cu <strong>{{mail}}</strong>, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp.\n    email:\n      label: E-mail nou\n      msg:\n        empty: Câmpul e-mail nu poate fi gol.\n  oauth:\n    connect: Conectează-te cu {{ auth_name }}\n    remove: Elimină {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Adăugați un e-mail de recuperare la contul dvs.\n    btn_update: Actualizare adresă de e-mail\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail-ul nu poate fi gol.\n    modal_title: E-mail deja existent.\n    modal_content: Această adresă de e-mail este deja înregistrată. Sigur doriți să vă conectați la contul existent?\n    modal_cancel: Schimbați e-mailul\n    modal_confirm: Conectează-te la contul existent\n  password_reset:\n    page_title: Resetează parola\n    btn_name: Resetează-mi parola\n    reset_success: >-\n      Ați schimbat cu succes parola; veți fi redirecționat către pagina de conectare.\n    link_invalid: >-\n      Ne pare rău, acest link de resetare a parolei nu mai este valabil. Poate că parola este deja resetată?\n    to_login: Continuă autentificarea în pagină\n    password:\n      label: Parolă\n      msg:\n        empty: Parola nu poate fi goală.\n        length: Lungimea trebuie să fie între 8 și 32\n        different: Parolele introduse pe ambele părți sunt incompatibile\n    password_confirm:\n      label: Confirmă parola nouă\n  settings:\n    page_title: Setări\n    goto_modify: Du-te pentru a modifica\n    nav:\n      profile: Profil\n      notification: Notificări\n      account: Cont\n      interface: Interfață\n    profile:\n      heading: Profil\n      btn_name: Salvează\n      display_name:\n        label: Nume afișat\n        msg: Numele afișat nu poate fi gol.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Nume de utilizator\n        caption: Oamenii te pot menționa ca \"@utilizator\".\n        msg: Numele de utilizator nu poate fi gol.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Imaginea de profil\n        gravatar: Gravatar\n        gravatar_text: Poți schimba imaginea pe\n        custom: Personalizat\n        custom_text: Poți să încarci imaginea.\n        default: Sistem\n        msg: Te rugăm să încarci un avatar\n      bio:\n        label: Despre mine\n      website:\n        label: Website\n        placeholder: \"https://exemplu.com\"\n        msg: Format incorect pentru website\n      location:\n        label: Locație\n        placeholder: \"Oraş, Ţară\"\n    notification:\n      heading: Notificări prin e-mail\n      turn_on: Pornire\n      inbox:\n        label: Notificări primite\n        description: Răspunde la întrebări, comentarii, invitații și multe altele.\n      all_new_question:\n        label: Adauga o intrebare noua\n        description: Primiți notificări despre toate întrebările noi. Până la 50 de întrebări pe săptămână.\n      all_new_question_for_following_tags:\n        label: Toate întrebările noi pentru etichetele următoare\n        description: Primiți notificări despre întrebări noi pentru următoarele etichete.\n    account:\n      heading: Cont\n      change_email_btn: Schimbați e-mailul\n      change_pass_btn: Schimbați parola\n      change_email_info: >-\n        Am trimis un e-mail la acea adresă. Vă rugăm să urmați instrucțiunile de confirmare.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Parolă actuală\n        msg: Parola nu poate fi goală.\n      password_title: Parolă\n      current_pass:\n        label: Parola curentă\n        msg:\n          empty: Parola curentă nu poate fi goală.\n          length: Lungimea trebuie să fie între 8 și 32.\n          different: Cele două parole introduse nu se potrivesc.\n      new_pass:\n        label: Parola nouă\n      pass_confirm:\n        label: Confirmă parola nouă\n    interface:\n      heading: Interfață\n      lang:\n        label: Limba interfeței\n        text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina.\n    my_logins:\n      title: Autentificările mele\n      label: Autentifică-te sau înregistrează-te pe acest site folosind aceste conturi.\n      modal_title: Elimină autentificarea\n      modal_content: Sunteţi sigur că doriţi să eliminaţi această autentificare din contul dumneavoastră?\n      modal_confirm_btn: Eliminare\n      remove_success: Eliminată cu succes\n  toast:\n    update: actualizare reușită\n    update_password: Parola schimbata cu succes.\n    flag_success: Mulțumim pentru marcare.\n    forbidden_operate_self: Interzis să operezi singur\n    review: Revizuirea ta va arăta după recenzie.\n    sent_success: Trimis cu succes\n  related_question:\n    title: Related\n    answers: răspunsuri\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Persoane întrebate\n    desc: Invită persoane care crezi că știu răspunsul.\n    invite: Invită să răspundă\n    add: Adaugă persoane\n    search: Caută persoane\n  question_detail:\n    action: Acţiune\n    created: Created\n    Asked: Întrebat\n    asked: întrebat\n    update: Modificat\n    Edited: Edited\n    edit: editat\n    commented: commented\n    Views: Văzute\n    Follow: Urmărește\n    Following: Urmăriți\n    follow_tip: Urmărește această întrebare pentru a primi notificări\n    answered: răspunse\n    closed_in: Închis în\n    show_exist: Arată întrebarea existentă.\n    useful: Utilă\n    question_useful: Acest lucru este util și clar\n    question_un_useful: Nu este clar sau nu este util\n    question_bookmark: Marchează această întrebare\n    answer_useful: Este util\n    answer_un_useful: Nu este util\n    answers:\n      title: Răspunsuri\n      score: Scor\n      newest: Cele mai noi\n      oldest: Cel mai vechi\n      btn_accept: Acceptă\n      btn_accepted: Acceptă\n    write_answer:\n      title: Răspunsul tău\n      edit_answer: Editează răspunsul meu existent\n      btn_name: Postează răspunsul tău\n      add_another_answer: Adaugă un alt răspuns\n      confirm_title: Continuă să răspunzi\n      continue: Continuare\n      confirm_info: >-\n        <p>Sunteţi sigur că doriţi să adăugaţi un alt răspuns?</p><p>Puteţi folosi link-ul de editare pentru a perfecţiona şi îmbunătăţi răspunsul existent, în schimb.</p>\n      empty: Răspunsul nu poate fi gol.\n      characters: conţinutul trebuie să aibă cel puţin 6 caractere.\n      tips:\n        header_1: Îți mulțumim pentru răspuns\n        li1_1: Asigurați-vă că <strong>răspundeți la întrebarea</strong>. Furnizați detalii și împărtășiți cercetările dvs.\n        li1_2: Faceți o copie de rezervă cu referințe sau experiență personală.\n        header_2: Dar <strong>evită</strong>...\n        li2_1: Solicită ajutor, caută clarificări sau răspunsuri la alte răspunsuri.\n    reopen:\n      confirm_btn: Redeschide\n      title: Redeschide această postare\n      content: Sunteţi sigur că doriţi să redeschideţi?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Fixează această postare\n      content: Sunteţi sigur că doriţi să fixaţi la nivel global? Acest post va apărea în partea de sus a tuturor listelor de postări.\n      confirm_btn: Fixează\n  delete:\n    title: Șterge această postare\n    question: >-\n      Nu recomandăm <strong>ștergerea întrebărilor cu răspunsuri</strong> deoarece acest lucru privează viitorii cititori de aceste cunoștințe.</p><p>Ștergerea repetată a întrebărilor cu răspuns poate duce la blocarea contului dvs. de a întreba. Sigur doriți să ștergeți?\n    answer_accepted: >-\n      <p>Nu recomandăm <strong>ștergerea răspunsului acceptat</strong> deoarece acest lucru privează viitorii cititori de aceste cunoștințe. </p> Ștergerea repetată a răspunsurilor acceptate poate duce la blocarea contului dvs. de a răspunde. Sigur doriți să ștergeți?\n    other: Sunteţi sigur că doriţi să ştergeţi?\n    tip_answer_deleted: Aceasta postare a fost stearsa\n    undelete_title: Anulează ștergerea acestei postări\n    undelete_desc: Sunteți sigur că doriți să adulați ștergerea?\n  btns:\n    confirm: Confirmați\n    cancel: Anulați\n    edit: Editează\n    save: Salvează\n    delete: Ștergeți\n    undelete: Restabilește\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Autentifică-te\n    signup: Înscrieți-vă\n    logout: Deconectaţi-vă\n    verify: Verificare\n    create: Create\n    approve: Aprobă\n    reject: Respins\n    skip: Treci peste\n    discard_draft: Respingeți draftul\n    pinned: Fixat\n    all: Toate\n    question: Întrebare\n    answer: Răspuns\n    comment: Comentariu\n    refresh: Actualizare\n    resend: Retrimite\n    deactivate: Dezactivare\n    active: Activați\n    suspend: Suspendați\n    unsuspend: Anulează suspendare\n    close: Închide\n    reopen: Redeschide\n    ok: OK\n    light: Luminoasă\n    dark: Întunecată\n    system_setting: Setări de sistem\n    default: Default\n    reset: Resetează\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Rezultatele căutării\n    keywords: Cuvinte cheie\n    options: Opţiuni\n    follow: Urmărește\n    following: Urmăriți\n    counts: \"{{count}} rezultatele\"\n    counts_loading: \"... Results\"\n    more: Mai mult\n    sort_btns:\n      relevance: Relevanță\n      newest: Cele mai noi\n      active: Activ\n      score: Scor\n      more: Mai mult\n    tips:\n      title: Sfaturi de căutare avansate\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>utilizator:username</1> căutare de către autor\"\n      answer: \"<1>răspunsuri:0</1> întrebări fără răspuns\"\n      score: \"<1>scor:3</1> postări cu un scor de 3+\"\n      question: \"<1>este:question</1> întrebări de căutare\"\n      is_answer: \"<1>este:răspuner</1> răspunsuri la căutare\"\n    empty: Nu am putut găsi nimic. <br /> Încearcă cuvinte cheie diferite sau mai puţin specifice.\n  share:\n    name: Distribuiți\n    copy: Copiază linkul\n    via: Distribuie postarea prin...\n    copied: Copiat\n    facebook: Partajează pe Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Nu poți vota pentru propria ta postare.\n  modal_confirm:\n    title: Eroare...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Noul tău cont este confirmat; vei fi redirecționat către pagina de pornire.\n    link: Continuă la pagina principală\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: E-mailul dvs. a fost actualizat.\n    confirm_new_email_invalid: >-\n      Ne pare rău, acest link de confirmare nu mai este valabil. Poate că e-mailul dvs. a fost deja modificat?\n  unsubscribe:\n    page_title: Dezabonează-te\n    success_title: Dezabonare cu succes\n    success_desc: Ați fost eliminat cu succes din această listă de abonați și nu veți mai primi alte e-mailuri de la noi.\n    link: Modificați setările\n  question:\n    following_tags: Etichete urmărite\n    edit: Editează\n    save: Salvează\n    follow_tag_tip: Urmărește etichetele pentru a curăța lista ta de întrebări.\n    hot_questions: Întrebări importante\n    all_questions: Toate întrebările\n    x_questions: \"{{ count }} Întrebări\"\n    x_answers: \"{{ count }} răspunsuri\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Întrebări\n    answers: Răspunsuri\n    newest: Cele mai noi\n    active: Activ\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Scor\n    unanswered: Fără răspuns\n    modified: modificat\n    answered: răspunse\n    asked: întrebat\n    closed: închise\n    follow_a_tag: Urmărește o etichetă\n    more: Mai multe\n  personal:\n    overview: Privire de ansamblu\n    answers: Răspunsuri\n    answer: răspuns\n    questions: Întrebări\n    question: întrebare\n    bookmarks: Semne de carte\n    reputation: Reputație\n    comments: Comentarii\n    votes: Voturi\n    badges: Badges\n    newest: Cele mai noi\n    score: Scor\n    edit_profile: Editare profil\n    visited_x_days: \"{{ count }} zile vizitate\"\n    viewed: Văzute\n    joined: Înscris\n    comma: \",\"\n    last_login: Văzut\n    about_me: Despre mine\n    about_me_empty: \"// Salut, Lumea !\"\n    top_answers: Top răspunsuri\n    top_questions: Top Intrebari\n    stats: Statistici\n    list_empty: Nici o postare găsită.<br />Poate doriţi să selectaţi o filă diferită?\n    content_empty: No posts found.\n    accepted: Acceptat\n    answered: răspunse\n    asked: întrebat\n    downvoted: vot negativ\n    mod_short: MOD\n    mod_long: Moderatori\n    x_reputation: reputație\n    x_votes: voturi primite\n    x_answers: răspunsuri\n    x_questions: întrebări\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Înainte\n    done: Finalizat\n    config_yaml_error: Nu se poate crea fișierul config.yaml.\n    lang:\n      label: Vă rugăm să selectați limba\n    db_type:\n      label: Motorul Bazei de Date\n    db_username:\n      label: Nume de utilizator\n      placeholder: root\n      msg: Numele de utilizator nu poate fi gol.\n    db_password:\n      label: Parolă\n      placeholder: root\n      msg: Parola nu poate fi goală.\n    db_host:\n      label: Numele serverului de baze de date\n      placeholder: \"db:3306\"\n      msg: Adresa bazei de date nu poate fi goală.\n    db_name:\n      label: Numele bazei de date\n      placeholder: răspuns\n      msg: Numele bazei de date nu poate fi gol.\n    db_file:\n      label: Fișierul bazei de date\n      placeholder: /data/answer.db\n      msg: Fişierul bazei de date nu poate fi gol.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Crează config.yaml\n      label: Fișierul config.yaml a fost creat.\n      desc: >-\n        Puteți crea fișierul <1>config.yaml</1> manual în directorul <1>/var/wwww/xxx/</1> și inserați următorul text în el.\n      info: După ce ai făcut asta, apasă butonul \"Înainte\".\n    site_information: Informatii site\n    admin_account: Contul de admin\n    site_name:\n      label: Numele site-ului\n      msg: Numele site-ului nu poate fi gol.\n      msg_max_length: Numele site-ului trebuie să aibă maximum 30 de caractere lungime.\n    site_url:\n      label: URL-ul site-ului\n      text: Adresa site-ului dvs.\n      msg:\n        empty: URL-ul site-ului nu poate fi gol.\n        incorrect: Format incorect pentru URL-ul site-ului.\n        max_length: URL-ul site-ului trebuie să aibă maximum 512 caractere lungime.\n    contact_email:\n      label: E-mail de contact\n      text: Adresa de e-mail a persoanei cheie responsabile pentru acest site.\n      msg:\n        empty: E-mailul de contact nu poate fi gol.\n        incorrect: E-mail de contact are un format incorect.\n    login_required:\n      label: Privat\n      switch: Autentificare necesară\n      text: Numai utilizatorii autentificați pot accesa această comunitate.\n    admin_name:\n      label: Nume\n      msg: Câmpul Nume trebuie completat.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Parolă\n      text: >-\n        Veți avea nevoie de această parolă pentru autentificare. Vă rugăm să o păstrați într-o locație sigură.\n      msg: Parola nu poate fi goală.\n      msg_min_length: Parola trebuie să aibă cel puțin 8 caractere.\n      msg_max_length: Parola trebuie să aibă cel puțin 32 caractere.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: E-mail\n      text: Veţi avea nevoie de acest e-mail pentru a vă autentifica.\n      msg:\n        empty: Câmpul e-mail nu poate fi gol.\n        incorrect: Campul e-mail are formatul incorect.\n    ready_title: Your site is ready\n    ready_desc: >-\n      Dacă te simți vreodată ca și cum ai schimba mai multe setări, vizitează <1>secțiunea de administrare </1>; găsește-o în meniul site-ului.\n    good_luck: \"Distracție plăcută și noroc!\"\n    warn_title: Atenţie\n    warn_desc: >-\n      Fișierul <1>config.yaml</1> există deja. Dacă trebuie să resetați oricare dintre elementele de configurare din acest fișier, vă rugăm să îl ștergeți mai întâi.\n    install_now: Puteți încerca <1>să instalați acum</1>.\n    installed: Deja instalat\n    installed_desc: >-\n      Se pare că ai instalat deja. Pentru a reinstala vă rugăm să ștergeți mai întâi vechile tabele ale bazei de date.\n    db_failed: Conexiunea la baza de date a eșuat\n    db_failed_desc: >-\n      Acest lucru înseamnă fie că baza de date este în configurația ta <1>. config yaml</1> este incorect sau contactul cu serverul bazei de date nu a putut fi stabilit. Acest lucru ar putea însemna că serverul gazdei nu este în funcțiune.\n  counts:\n    views: vizualizări\n    votes: voturi\n    answers: răspunsuri\n    accepted: Acceptat\n  page_error:\n    http_error: HTTP - {{ code }}\n    desc_403: Nu ai permisiunea să accesezi pagina.\n    desc_404: Din păcate, această pagină nu există.\n    desc_50X: Serverul a întâmpinat o eroare și nu a putut finaliza cererea.\n    back_home: Înapoi la pagina principală\n  page_maintenance:\n    desc: \"Suntem în mentenanță, ne vom întoarce în curând.\"\n  nav_menus:\n    dashboard: Panou de control\n    contents: Conţinut\n    questions: Întrebări\n    answers: Răspunsuri\n    users: Utilizatori\n    badges: Badges\n    flags: Marcaj\n    settings: Setări\n    general: General\n    interface: Interfață\n    smtp: SMTP\n    branding: Marcă\n    legal: Juridic\n    write: Scrie\n    terms: Terms\n    tos: Condiții de utilizare\n    privacy: Confidențialitate\n    seo: SEO\n    customize: Personalizează\n    themes: Teme\n    login: Autentifică-te\n    privileges: Privilegii\n    plugins: Extensii\n    installed_plugins: Extensii instalate\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Bun venit la {{site_name}}\n  user_center:\n    login: Autentifică-te\n    qrcode_login_tip: Vă rugăm să folosiți {{ agentName }} pentru a scana codul QR și a vă autentifica.\n    login_failed_email_tip: Autentificare eșuată. Vă rugăm să permiteți acestei aplicații să vă acceseze informațiile de e-mail înainte de a încerca din nou.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Administrator\n    dashboard:\n      title: Panou de control\n      welcome: Welcome to Admin!\n      site_statistics: Statisticile site-ului\n      questions: \"Întrebări:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Răspunsuri:\"\n      comments: \"Comentarii:\"\n      votes: \"Voturi:\"\n      users: \"Utilizatori:\"\n      flags: \"Marcaje:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Versiune:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Director încărcare:\"\n      run_mode: \"Modul de rulare:\"\n      private: Privat\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Fusul orar:\"\n      system_info: Informaţii despre sistem\n      go_version: \"Versiune Go:\"\n      database: \"Baza de date:\"\n      database_size: \"Dimensiune bază de date:\"\n      storage_used: \"Spațiu utilizat:\"\n      uptime: \"Timpul de funcționare:\"\n      links: Links\n      plugins: Pluginuri\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Ducumente\n      feedback: Feedback\n      support: Suport\n      review: Recenzie\n      config: Configurație\n      update_to: Actualizare la\n      latest: Recente\n      check_failed: Verificarea eșuată\n      \"yes\": \"Da\"\n      \"no\": \"Nu\"\n      not_allowed: Nu este permis\n      allowed: Permis\n      enabled: Activat\n      disabled: Dezactivat\n      writable: Inscriptibil\n      not_writable: Neinscriptibil\n    flags:\n      title: Steaguri\n      pending: În așteptare\n      completed: Finalizată\n      flagged: Semnalizat\n      flagged_type: Marcat {{ type }}\n      created: Creată\n      action: Actiune\n      review: Recenzie\n    user_role_modal:\n      title: Schimbă rolul utilizatorului la...\n      btn_cancel: Anulați\n      btn_submit: Trimiteți\n    new_password_modal:\n      title: Setați parola nouă\n      form:\n        fields:\n          password:\n            label: Parolă\n            text: Utilizatorul va fi deconectat și trebuie să se conecteze din nou.\n            msg: Parola trebuie să aibă o lungime de 8-32 caractere.\n      btn_cancel: Anulați\n      btn_submit: Trimiteți\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Adaugă un nou utilizator\n      form:\n        fields:\n          users:\n            label: Adăugare utilizator în bloc\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separați “nume, email, parola” cu virgulă. Un utilizator pe linie.\n            msg: \"Te rugăm să introduci e-mailul utilizatorului, câte unul pe linie.\"\n          display_name:\n            label: Nume afișat\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: E-mail\n            msg: E-mail nu este validă.\n          password:\n            label: Parolă\n            msg: Parola trebuie să aibă o lungime de 8-32 caractere.\n      btn_cancel: Anulați\n      btn_submit: Trimiteți\n    users:\n      title: Utilizatori\n      name: Nume\n      email: E-mail\n      reputation: Reputație\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Stare\n      role: Rol\n      action: Acţiune\n      change: Schimbare\n      all: Toate\n      staff: Personal\n      more: Mai mult\n      inactive: Inactiv\n      suspended: Suspendat\n      deleted: Şters\n      normal: Normal\n      Moderator: Moderator\n      Admin: Administrator\n      User: Utilizator\n      filter:\n        placeholder: \"Filtrare după nume, utilizator:id\"\n      set_new_password: Setați parola nouă\n      edit_profile: Edit profile\n      change_status: Schimba starea\n      change_role: Schimbare rol\n      show_logs: Arată jurnalele\n      add_user: Adaugă utilizator\n      deactivate_user:\n        title: Dezactivare utilizator\n        content: Un utilizator inactiv trebuie să își revalideze e-mailul.\n      delete_user:\n        title: Ștergeți acest utilizator\n        content: Sunteţi sigur că doriţi să ştergeţi acest utilizator? Acest lucru este permanent!\n        remove: Eliminaţi acest conţinut\n        label: Elimină toate întrebările, răspunsurile, comentariile etc.\n        text: Nu bifați acest lucru dacă doriți să ștergeți doar contul utilizatorului.\n      suspend_user:\n        title: Suspendă acest utilizator\n        content: Un utilizator suspendat nu se poate autentifica.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Întrebări\n      unlisted: Unlisted\n      post: Postare\n      votes: Voturi\n      answers: Răspunsuri\n      created: Creată\n      status: Stare\n      action: Actiune\n      change: Schimbare\n      pending: În așteptare\n      filter:\n        placeholder: \"Filtrează după titlu, întrebare: id\"\n    answers:\n      page_title: Răspunsuri\n      post: Postare\n      votes: Voturi\n      created: Creată\n      status: Stare\n      action: Acţiune\n      change: Schimbare\n      filter:\n        placeholder: \"Filtrează după titlu, întrebare: id\"\n    general:\n      page_title: General\n      name:\n        label: Numele site-ului\n        msg: Numele site-ului nu poate fi gol.\n        text: \"Numele acestui site, așa cum este folosit în eticheta de titlu.\"\n      site_url:\n        label: URL-ul site-ului\n        msg: Url-ul site-ului nu poate fi gol.\n        validate: Introduceți un URL valid.\n        text: Adresa site-ului dvs.\n      short_desc:\n        label: Scurtă descriere a site-ului\n        msg: Scurtă descriere a site-ului nu poate fi goală.\n        text: \"Scurtă descriere, așa cum este folosit în eticheta titlu pe website.\"\n      desc:\n        label: Descriere site\n        msg: Descrierea site-ului nu poate fi goală.\n        text: \"Descrie acest site într-o propoziție, așa cum este folosit în tag-ul meta descriere.\"\n      contact_email:\n        label: E-mail de contact\n        msg: E-mailul de contact nu poate fi gol.\n        validate: E-mailul de contact nu este valid.\n        text: Adresa de e-mail a persoanei cheie responsabile pentru acest site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interfață\n      language:\n        label: Limba interfeței\n        msg: Limba interfata nu poate fi goala.\n        text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina.\n      time_zone:\n        label: Fusul orar\n        msg: Fusul orar nu poate fi gol.\n        text: Alege un oraș în același fus orar cu tine.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: De la e-mail\n        msg: Câmpul e-mail nu poate fi gol.\n        text: Adresa de e-mail de la care e-mailurile sunt trimise.\n      from_name:\n        label: Din numele\n        msg: Numele nu poate fi gol.\n        text: Numele de la care sunt trimise e-mailurile.\n      smtp_host:\n        label: Gazda SMTP\n        msg: Gazda SMTP nu poate fi goală.\n        text: Serverul tau de mail.\n      encryption:\n        label: Criptare\n        msg: Câmpul decriptare nu poate fi gol.\n        text: Pentru majoritatea serverelor SSL este opțiunea recomandată.\n        ssl: SSL\n        tls: TLS\n        none: Niciuna\n      smtp_port:\n        label: Portul SMTP\n        msg: Portul SMTP trebuie să fie numărul 1 ~ 65535.\n        text: Portul către serverul de mail.\n      smtp_username:\n        label: Utilizatorul SMTP\n        msg: Numele de utilizator SMTP nu poate fi gol.\n      smtp_password:\n        label: Parola SMTP\n        msg: Parola SMTP nu poate fi goală.\n      test_email_recipient:\n        label: Destinatari de e-mail test\n        text: Furnizați adresa de e-mail care va primi trimiterile de teste.\n        msg: Destinatarii de e-mail de test sunt invalizi\n      smtp_authentication:\n        label: Activare Authenticator\n        title: Autentificare SMTP\n        msg: Autentificarea SMTP nu poate fi goală.\n        \"yes\": \"Da\"\n        \"no\": \"Nu\"\n    branding:\n      page_title: Marcă\n      logo:\n        label: Logo\n        msg: Logo-ul nu poate fi gol.\n        text: Imaginea logo-ului din stânga sus a site-ului dvs. Utilizaţi o imagine dreptunghiulară largă cu o înălţime de 56 şi un raport de aspect mai mare de 3:1. Dacă nu se completează, textul pentru titlul site-ului va fi afișat.\n      mobile_logo:\n        label: Logo mobil\n        text: Logo-ul folosit pe versiunea mobila a site-ului dvs. Utilizați o imagine dreptunghiulară largă cu o înălțime de 56. Dacă nu o completați, va fi utilizată imaginea din setarea \"logo\".\n      square_icon:\n        label: Pictogramă pătrată\n        msg: Pictograma pătrată nu poate fi goală.\n        text: Imaginea folosită ca bază pentru pictogramele de metadate. Ar trebui să fie, în mod ideal, mai mare de 512x512.\n      favicon:\n        label: Favicon\n        text: O pictogramă favorită pentru site-ul dvs. Pentru a lucra corect peste un CDN trebuie să fie un png. Va fi redimensionată la 32x32. Dacă este lăsat necompletat, va fi folosită \"iconiță pătrată\".\n    legal:\n      page_title: Juridic\n      terms_of_service:\n        label: Termeni și condiții\n        text: \"Puteți adăuga aici termeni de conținut pentru serviciu. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici.\"\n      privacy_policy:\n        label: Politică de confidențialitate\n        text: \"Puteți adăuga aici termeni politicii de confidențialitate. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Fiecare utilizator poate scrie doar câte un răspuns pentru fiecare întrebare\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Etichete recomandate\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Fiecare întrebare nouă trebuie să aibă cel puțin o etichetă recomandată.\"\n      reserved_tags:\n        label: Etichete rezervate\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Legătură permanenta\n        text: Structurile URL personalizate pot îmbunătăți capacitatea de utilizare și compatibilitatea link-urilor tale.\n      robots:\n        label: robots.txt\n        text: Acest lucru va suprascrie permanent orice setări ale site-ului.\n    themes:\n      page_title: Teme\n      themes:\n        label: Teme\n        text: Selectaţi o temă existentă.\n      color_scheme:\n        label: Paletă de culori\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Culoare primară\n        text: Modifică culorile folosite de temele tale\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS și HTML\n      custom_css:\n        label: CSS personalizat\n        text: >\n\n      head:\n        label: Cap\n        text: >\n\n      header:\n        label: Antet\n        text: >\n\n      footer:\n        label: Subsol\n        text: Se va insera înainte de &lt;/body>.\n      sidebar:\n        label: Bară laterală\n        text: Acesta va fi inserat în bara laterală.\n    login:\n      page_title: Autentifică-te\n      membership:\n        title: Calitatea de membru\n        label: Permite înregistrări noi\n        text: Dezactivați pentru a împiedica pe oricine să creeze un cont nou.\n      email_registration:\n        title: Înregistrare e-mail\n        label: Permite înregistrări noi\n        text: Dezactivați pentru a preveni crearea unui cont nou prin e-mail.\n      allowed_email_domains:\n        title: Domenii de e-mail permise\n        text: Domeniile de e-mail cu care utilizatorii trebuie să înregistreze conturi. Un domeniu pe linie. Se ignoră atunci când este gol.\n      private:\n        title: Privat\n        label: Autentificare necesară\n        text: Numai utilizatorii autentificați pot accesa această comunitate.\n      password_login:\n        title: Parola de login\n        label: Permiteți autentificarea prin e-mail și parolă\n        text: \"AVERTISMENT: Dacă opriți, este posibil să nu vă puteți conecta dacă nu ați configurat anterior o altă metodă de autentificare.\"\n    installed_plugins:\n      title: Extensii instalate\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: Toate\n        active: Activ\n        inactive: Inactiv\n        outdated: Învechit\n      plugins:\n        label: Extensii\n        text: Selectați un plugin existent.\n      name: Nume\n      version: Versiune\n      status: Stare\n      action: Acţiune\n      deactivate: Dezactivare\n      activate: Activare\n      settings: Setări\n    settings_users:\n      title: Utilizatori\n      avatar:\n        label: Avatarul implicit\n        text: Pentru utilizatorii fără un avatar personalizat propriu.\n      gravatar_base_url:\n        label: URL de bază Gravatar\n        text: URL-ul bazei API a furnizorului de Gravatar. Ignorat când este gol.\n      profile_editable:\n        title: Profil editabil\n      allow_update_display_name:\n        label: Permite utilizatorilor să își schimbe numele afișat\n      allow_update_username:\n        label: Permite utilizatorilor să își schimbe numele de utilizator\n      allow_update_avatar:\n        label: Permite utilizatorilor să își schimbe imaginea de profil\n      allow_update_bio:\n        label: Permite utilizatorilor să își schimbe propriul lor despre mine\n      allow_update_website:\n        label: Permite utilizatorilor să îşi schimbe website-ul\n      allow_update_location:\n        label: Permite utilizatorilor să își schimbe locația\n    privilege:\n      title: Privilegii\n      level:\n        label: Nivel necesar de reputație\n        text: Alegeți reputația necesară pentru privilegii\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (opțional)\n    empty: nu poate fi lăsat necompletat\n    invalid: nu este valid\n    btn_submit: Salvează\n    not_found_props: \"Proprietatea solicitată {{ key }} nu a fost găsită.\"\n    select: Selectează\n  page_review:\n    review: Recenzie\n    proposed: propus\n    question_edit: Editare întrebări\n    answer_edit: Editările răspunsului\n    tag_edit: Editare etichetă\n    edit_summary: Editează sumarul\n    edit_question: Editați întrebarea\n    edit_answer: Editați răspunsul\n    edit_tag: Editare etichetă\n    empty: Nu au mai rămas sarcini de evaluare.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Tip\n    reputation: reputation\n    flag_post_type: Acest post a fost marcat de {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Editează postarea\n    list_post: Listează postarea\n    unlist_post: Unlist post\n  timeline:\n    undeleted: restabilește\n    deleted: şterse\n    downvote: vot negativ\n    upvote: vot pozitiv\n    accept: acceptat\n    cancelled: anulat\n    commented: comentat\n    rollback: revenire\n    edited: editat\n    answered: răspunse\n    asked: întrebat\n    closed: închise\n    reopened: redeschise\n    created: creat\n    pin: fixat\n    unpin: nefixat\n    show: listă\n    hide: nelistat\n    title: \"Istoric pentru\"\n    tag_title: \"Cronologie pentru\"\n    show_votes: \"Afișare voturi\"\n    n_or_a: N/A\n    title_for_question: \"Cronologie pentru\"\n    title_for_answer: \"Calendarul răspunsului la {{ title }} cu {{ author }}\"\n    title_for_tag: \"Cronologie pentru etichetă\"\n    datetime: Dată și oră\n    type: Tip\n    by: De către\n    comment: Comentariu\n    no_data: \"Nu a fost găsit nimic.\"\n  users:\n    title: Utilizatori\n    users_with_the_most_reputation: Utilizatori cu cele mai mari scoruri ale reputaţiei în această săptămână\n    users_with_the_most_vote: Utilizatorii care au votat cel mai mult săptămâna aceasta\n    staffs: Personalul acestei comunități\n    reputation: reputație\n    votes: voturi\n  prompt:\n    leave_page: Sunteți sigur că doriți să ieșiți din pagina asta?\n    changes_not_save: Modificările nu pot fi salvate.\n  draft:\n    discard_confirm: Ești sigur că vrei să renunți la ciornă?\n  messages:\n    post_deleted: Această postare a fost ștearsă.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: Această postare a fost fixată.\n    post_unpin: Această postare nu a fost fixată.\n    post_hide_list: Această postare a fost ascunsă din listă.\n    post_show_list: Această postare a fost afișată în listă.\n    post_reopen: Această postare a fost redeschisă.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/ru_RU.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Выполнено.\n    unknown:\n      other: Неизвестная ошибка.\n    request_format_error:\n      other: Формат файла не корректен.\n    unauthorized_error:\n      other: Авторизация не выполнена.\n    database_error:\n      other: Ошибка сервера данных.\n    forbidden_error:\n      other: Доступ запрещен.\n    duplicate_request_error:\n      other: Дублирующая отправка.\n  action:\n    report:\n      other: Пожаловаться\n    edit:\n      other: Редактировать\n    delete:\n      other: Удалить\n    close:\n      other: Закрыть\n    reopen:\n      other: Открыть\n    forbidden_error:\n      other: Доступ запрещен.\n    pin:\n      other: Закрепить\n    hide:\n      other: Убрать\n    unpin:\n      other: Открепить\n    show:\n      other: Список\n    invite_someone_to_answer:\n      other: Редактировать\n    undelete:\n      other: Отменить удаление\n    merge:\n      other: Объединить\n  role:\n    name:\n      user:\n        other: Пользователь\n      admin:\n        other: Администратор\n      moderator:\n        other: Модератор\n    description:\n      user:\n        other: По умолчанию, без специального доступа.\n      admin:\n        other: Имейте все полномочия для доступа к сайту.\n      moderator:\n        other: Имеет доступ ко всем сообщениям, кроме настроек администратора.\n  privilege:\n    level_1:\n      description:\n        other: Уровень 1 (для приватной команды, группы требуется наименьшая репутация)\n    level_2:\n      description:\n        other: Уровень 2 (для стартапа достаточен низкий уровень репутации)\n    level_3:\n      description:\n        other: Уровень 3 (для зрелого сообщества требуется высокая репутация)\n    level_custom:\n      description:\n        other: Настраиваемый уровень\n    rank_question_add_label:\n      other: Задать вопрос\n    rank_answer_add_label:\n      other: Написать ответ\n    rank_comment_add_label:\n      other: Написать комментарий\n    rank_report_add_label:\n      other: Пожаловаться\n    rank_comment_vote_up_label:\n      other: Полезный комментарий\n    rank_link_url_limit_label:\n      other: Опубликовать более 2 ссылок за раз\n    rank_question_vote_up_label:\n      other: Полезный вопрос\n    rank_answer_vote_up_label:\n      other: Полезный ответ\n    rank_question_vote_down_label:\n      other: Бесполезный вопрос\n    rank_answer_vote_down_label:\n      other: Бесполезный ответ\n    rank_invite_someone_to_answer_label:\n      other: Пригласить кого-нибудь ответить\n    rank_tag_add_label:\n      other: Новый тег\n    rank_tag_edit_label:\n      other: Редактировать описание тега (требуется проверка)\n    rank_question_edit_label:\n      other: Редактировать вопрос другого пользователя (требуется проверка)\n    rank_answer_edit_label:\n      other: Редактировать ответ другого пользователя (требуется проверка)\n    rank_question_edit_without_review_label:\n      other: Редактировать вопрос другого пользователя без проверки\n    rank_answer_edit_without_review_label:\n      other: Редактировать ответ другого пользователя без проверки\n    rank_question_audit_label:\n      other: Проверить изменения вопроса\n    rank_answer_audit_label:\n      other: Проверить изменения ответа\n    rank_tag_audit_label:\n      other: Проверить изменения тегов\n    rank_tag_edit_without_review_label:\n      other: Редактировать описание тега без проверки\n    rank_tag_synonym_label:\n      other: Управлять синонимами тегов\n  email:\n    other: Эл. почта\n  e_mail:\n    other: Почта\n  password:\n    other: Пароль\n  pass:\n    other: Пароль\n  old_pass:\n    other: Current password\n  original_text:\n    other: Это сообщение\n  email_or_password_wrong_error:\n    other: Неверное имя пользователя или пароль.\n  error:\n    common:\n      invalid_url:\n        other: Неверная URL.\n      status_invalid:\n        other: Неверный статус.\n    password:\n      space_invalid:\n        other: Пароль не должен содержать пробелы.\n    admin:\n      cannot_update_their_password:\n        other: Вы не можете изменить свой пароль.\n      cannot_edit_their_profile:\n        other: Вы не можете изменять свой профиль.\n      cannot_modify_self_status:\n        other: Вы не можете изменить свой статус.\n      email_or_password_wrong:\n        other: Неверное имя пользователя или пароль.\n    answer:\n      not_found:\n        other: Ответ не найден.\n      cannot_deleted:\n        other: Недостаточно прав для удаления.\n      cannot_update:\n        other: Нет прав для обновления.\n      question_closed_cannot_add:\n        other: Вопросы закрыты и не могут быть добавлены.\n      content_cannot_empty:\n        other: Содержимое ответа не может быть пустым.\n    comment:\n      edit_without_permission:\n        other: Комментарий не может редактироваться.\n      not_found:\n        other: Комментарий не найден.\n      cannot_edit_after_deadline:\n        other: Невозможно редактировать комментарий из-за того, что он был создан слишком давно.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Адрес электронной почты уже существует.\n      need_to_be_verified:\n        other: Адрес электронной почты должен быть подтвержден.\n      verify_url_expired:\n        other: Срок действия подтверждённого адреса электронной почты истек, пожалуйста, отправьте письмо повторно.\n      illegal_email_domain_error:\n        other: Невозможно использовать email с этим доменом. Пожалуйста, используйте другой.\n    lang:\n      not_found:\n        other: Языковой файл не найден.\n    object:\n      captcha_verification_failed:\n        other: Captcha введена неверно.\n      disallow_follow:\n        other: Вы не можете подписаться.\n      disallow_vote:\n        other: Вы не можете голосовать.\n      disallow_vote_your_self:\n        other: Вы не можете голосовать за собственный отзыв.\n      not_found:\n        other: Объект не найден.\n      verification_failed:\n        other: Проверка не удалась.\n      email_or_password_incorrect:\n        other: Email или пароль не совпадают.\n      old_password_verification_failed:\n        other: Не удалось подтвердить старый пароль\n      new_password_same_as_previous_setting:\n        other: Пароль не может быть таким же как прежний.\n      already_deleted:\n        other: Этот пост был удален.\n    meta:\n      object_not_found:\n        other: Объект мета не найден\n    question:\n      already_deleted:\n        other: Этот пост был удалён.\n      under_review:\n        other: Ваш пост ожидает проверки. Он станет видимым после одобрения.\n      not_found:\n        other: Вопрос не найден.\n      cannot_deleted:\n        other: Недостаточно прав для удаления.\n      cannot_close:\n        other: Нет разрешения на закрытие.\n      cannot_update:\n        other: Нет разрешения на обновление.\n      content_cannot_empty:\n        other: .\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Ранг репутации не соответствует условию.\n      vote_fail_to_meet_the_condition:\n        other: Спасибо за отзыв. Вам нужно как минимум {{.Rank}} репутация для голосования.\n      no_enough_rank_to_operate:\n        other: Для этого вам нужна репутация {{.Rank}}.\n    report:\n      handle_failed:\n        other: Не удалось обработать отчет.\n      not_found:\n        other: Отчет не найден.\n    tag:\n      already_exist:\n        other: Тег уже существует.\n      not_found:\n        other: Тег не найден.\n      recommend_tag_not_found:\n        other: Рекомендуемый тег не существует.\n      recommend_tag_enter:\n        other: Пожалуйста, введите хотя бы один тег.\n      not_contain_synonym_tags:\n        other: Не должно содержать теги синонимы.\n      cannot_update:\n        other: Нет прав для обновления.\n      is_used_cannot_delete:\n        other: Вы не можете удалить метку, которая используется.\n      cannot_set_synonym_as_itself:\n        other: Вы не можете установить синоним текущего тега.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Поле отправителя не может содержать email адрес.\n    theme:\n      not_found:\n        other: Тема не найдена.\n    revision:\n      review_underway:\n        other: В настоящее время не удается редактировать версию, в очереди на проверку.\n      no_permission:\n        other: Разрешения на пересмотр нет.\n    user:\n      external_login_missing_user_id:\n        other: Сторонняя платформа не предоставляет уникальный идентификатор пользователя, поэтому вы не можете войти в систему, пожалуйста, свяжитесь с администратором веб-сайта.\n      external_login_unbinding_forbidden:\n        other: Пожалуйста, установите пароль для входа в свою учетную запись, прежде чем удалять этот логин.\n      email_or_password_wrong:\n        other:\n          other: Почта и пароль введены неправильно.\n      not_found:\n        other: Пользователь не найден.\n      suspended:\n        other: Пользователь был заблокирован.\n      username_invalid:\n        other: Недопустимое имя пользователя.\n      username_duplicate:\n        other: Имя пользователя уже используется.\n      set_avatar:\n        other: Не удалось установить аватар.\n      cannot_update_your_role:\n        other: Вы не можете изменить свою роль.\n      not_allowed_registration:\n        other: В данный момент регистрация на сайте выключена.\n      not_allowed_login_via_password:\n        other: В настоящее время вход на сайт по паролю отключен.\n      access_denied:\n        other: Доступ запрещен\n      page_access_denied:\n        other: У вас нет доступа к этой странице.\n      add_bulk_users_format_error:\n        other: \"Ошибка формата {{.Field}} рядом с '{{.Content}}' в строке {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Количество пользователей, которое Вы добавляете, должно быть в промежутке от 1 до {{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Не удалось прочитать конфигурацию\n    database:\n      connection_failed:\n        other: Ошибка подключения к базе данных\n      create_table_failed:\n        other: Не удалось создать таблицу\n    install:\n      create_config_failed:\n        other: Не удалось создать файл config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Неподдерживаемый формат файла.\n    site_info:\n      config_not_found:\n        other: Конфигурация сайта не найдена.\n    badge:\n      object_not_found:\n        other: Объект бейджа не найден\n  reason:\n    spam:\n      name:\n        other: Спам\n      desc:\n        other: Этот пост является рекламой или вандализмом. Он не полезен и не имеет отношения к текущей теме.\n    rude_or_abusive:\n      name:\n        other: Грубость или оскорбления\n      desc:\n        other: \"Человек может посчитать такое содержимое неподходящим для уважительной беседы.\"\n    a_duplicate:\n      name:\n        other: дубликат\n      desc:\n        other: Этот вопрос уже был задан, и на него уже был получен ответ.\n      placeholder:\n        other: Введите существующую ссылку на вопрос\n    not_a_answer:\n      name:\n        other: это не ответ\n      desc:\n        other: \"Это сообщение было опубликовано в качестве ответа, но оно не пытается ответить на вопрос. Возможно, оно должно быть отредактировано, дополнено, быть другим вопросом или удалено навсегда.\"\n    no_longer_needed:\n      name:\n        other: Не актуально\n      desc:\n        other: Этот комментарий устарел, носит разговорный характер или не имеет отношения к данному сообщению.\n    something:\n      name:\n        other: Прочее\n      desc:\n        other: Этот пост требует внимания администрации по другой причине, не перечисленной выше.\n      placeholder:\n        other: Уточните, что именно Вас беспокоит\n    community_specific:\n      name:\n        other: специфическая для сообщества причина\n      desc:\n        other: Этот вопрос не соответствует рекомендациям сообщества.\n    not_clarity:\n      name:\n        other: нуждается в деталях или ясности\n      desc:\n        other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме.\n    looks_ok:\n      name:\n        other: выглядит нормально\n      desc:\n        other: Этот пост хороший и достойного качества.\n    needs_edit:\n      name:\n        other: нуждается в редактировании, и я сделал это\n      desc:\n        other: Устраните проблемы с этим сообщением самостоятельно.\n    needs_close:\n      name:\n        other: требует закрытия\n      desc:\n        other: На закрытый вопрос нельзя ответить, но все равно можно редактировать, голосовать и комментировать.\n    needs_delete:\n      name:\n        other: требует удаления\n      desc:\n        other: Этот пост будет удален.\n  question:\n    close:\n      duplicate:\n        name:\n          other: спам\n        desc:\n          other: Этот вопрос был задан ранее и уже имеет ответ.\n      guideline:\n        name:\n          other: специфическая для сообщества причина\n        desc:\n          other: Этот вопрос не соответствует рекомендациям сообщества.\n      multiple:\n        name:\n          other: нуждается в деталях или ясности\n        desc:\n          other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме.\n      other:\n        name:\n          other: прочее\n        desc:\n          other: Для этого поста требуется другая причина, не указанная выше.\n    operation_type:\n      asked:\n        other: вопросы\n      answered:\n        other: отвеченные\n      modified:\n        other: измененные\n    deleted_title:\n      other: Удаленные вопросы\n    questions_title:\n      other: Вопросы\n  tag:\n    tags_title:\n      other: Теги\n    no_description:\n      other: Тег не имеет описания.\n  notification:\n    action:\n      update_question:\n        other: обновленные вопросы\n      answer_the_question:\n        other: отвеченные вопросы\n      update_answer:\n        other: обновленные ответы\n      accept_answer:\n        other: принятые ответы\n      comment_question:\n        other: Прокомментированные ответы\n      comment_answer:\n        other: прокоментированные ответы\n      reply_to_you:\n        other: отвеченные вам\n      mention_you:\n        other: с упоминанием вас\n      your_question_is_closed:\n        other: Ваш вопрос был закрыт\n      your_question_was_deleted:\n        other: Ваш вопрос был удален\n      your_answer_was_deleted:\n        other: Ваш ответ был удален\n      your_comment_was_deleted:\n        other: Ваш комментарий был удален\n      up_voted_question:\n        other: поддержанный вопрос\n      down_voted_question:\n        other: неподдержанный вопрос\n      up_voted_answer:\n        other: ответ \"за\"\n      down_voted_answer:\n        other: ответ \"против\"\n      up_voted_comment:\n        other: поддержанный комментарий\n      invited_you_to_answer:\n        other: пригласил вас ответить\n      earned_badge:\n        other: Вы заработали значок \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Подтвердите новый адрес электронной почты\"\n      body:\n        other: \"Подтвердите свой новый адрес электронной почты для {{.SiteName}}, перейдя по следующей ссылке:<br> <a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br> Если вы не запрашивали это изменение, пожалуйста, проигнорируйте это электронное письмо. <br><br> - <br> Примечание: Данное сообщение является автоматическим, отвечать на него не нужно.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} ответил на ваш вопрос\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Открыть {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Отписаться.</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} приглашает вас в Answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Я думаю, что вы можете знать ответ.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Открыть {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Отписаться</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} прокомментировал под вашей публикацией\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Открыть {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Отписаться</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Новый вопрос: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Пароль сброшен\"\n      body:\n        other: \"Кто-то попросил сбросить ваш пароль на {{.SiteName}}.<br><br>\\n\\nЕсли это не вы, вы можете проигнорировать это письмо.<br><br>\\n\\nПерейдите по следующей ссылке, чтобы выбрать новый пароль:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Подтвердите Ваш новый аккаунт\"\n      body:\n        other: \"Добро пожаловать в {{.SiteName}}!<br><br>\\n\\nПерейдите по следующей ссылке для подтверждения и активации вашей новой учетной записи:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nЕсли ссылка выше не нажата, попробуйте скопировать и вставить её в адресную строку вашего браузера.\\n<br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Проверочное электронное письмо\"\n      body:\n        other: \"Это тестовое сообщение.\\n<br><br>\\n\\n--<br>\\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.\"\n  action_activity_type:\n    upvote:\n      other: проголосовать за\n    upvoted:\n      other: проголосовано за\n    downvote:\n      other: бесполезный\n    downvoted:\n      other: проголосовано против\n    accept:\n      other: принять\n    accepted:\n      other: принято\n    edit:\n      other: редактировать\n  review:\n    queued_post:\n      other: Задан вопрос\n    flagged_post:\n      other: Отмеченный пост\n    suggested_post_edit:\n      other: Предложенные исправления\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} и {{ .Count }} еще...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Автобиограф\n        desc:\n          other: Заполнена информация об <a href=\"{{ .ProfileURL }}\" target=\"_blank\">профиле</a>.\n      certified:\n        name:\n          other: Сертифицированный\n        desc:\n          other: Завершил наше новое руководство пользователя.\n      editor:\n        name:\n          other: Редактор\n        desc:\n          other: Впервые отредактировать сообщение\n      first_flag:\n        name:\n          other: Первый флаг\n        desc:\n          other: Впервые проставить флаг в сообщения\n      first_upvote:\n        name:\n          other: Первый голос\n        desc:\n          other: Впервые добавить голос в сообщении.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Оставить 5 комментариев.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Прочтите [правила сообщества].\n      reader:\n        name:\n          other: Читатель\n        desc:\n          other: Прочитать каждый ответ в разделе с более чем 10 ответами.\n      welcome:\n        name:\n          other: Добро пожаловать\n        desc:\n          other: Получен голос «за».\n      nice_share:\n        name:\n          other: Неплохо поделился\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Спасибо\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Активный участник на год, опубликовал по крайней мере один раз.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: 'Форматирование:'\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Назад\n    next: Следующий\n  page_title:\n    question: Вопрос\n    questions: Вопросы\n    tag: Тэг\n    tags: Теги\n    tag_wiki: wiki тэг\n    create_tag: Создать тег\n    edit_tag: Изменить тег\n    ask_a_question: Create Question\n    edit_question: Редактировать вопрос\n    edit_answer: Редактировать ответ\n    search: Поиск\n    posts_containing: Посты содержащие\n    settings: Настройки\n    notifications: Уведомления\n    login: Вход\n    sign_up: Регистрация\n    account_recovery: Восстановление аккаунта\n    account_activation: Активация учётной записи\n    confirm_email: Подтвердить адрес электронной почты\n    account_suspended: Аккаунт заблокирован\n    admin: Управление\n    change_email: Изменить Email\n    install: Установка ответа\n    upgrade: Обновить ответ\n    maintenance: Обслуживание сайта\n    users: Пользователи\n    oauth_callback: Идет обработка\n    http_404: Ошибка HTTP 404\n    http_50X: Ошибка HTTP 500\n    http_403: Ошибка HTTP 403\n    logout: Выйти\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Уведомления\n    inbox: Входящие\n    achievement: Достижения\n    new_alerts: Новые оповещения\n    all_read: Отметить всё как прочитанное\n    show_more: Показать еще\n    someone: Кто-то\n    inbox_type:\n      all: Все\n      posts: Посты\n      invites: Приглашения\n      votes: Голоса\n    answer: Ответ\n    question: Вопрос\n    badge_award: Значок\n  suspended:\n    title: Ваш аккаунт заблокирован\n    until_time: \"Ваша учетная запись была заблокирована до {{ time }}.\"\n    forever: Этот пользователь был навсегда заблокирован.\n    end: Вы не соответствуете правилам сообщества.\n    contact_us: Связаться с нами\n  editor:\n    blockquote:\n      text: Цитата\n    bold:\n      text: Сильный\n    chart:\n      text: Диаграмма\n      flow_chart: Блок-схема\n      sequence_diagram: Диаграмма последовательности\n      class_diagram: Диаграмма классов\n      state_diagram: Диаграмма состояний\n      entity_relationship_diagram: Диаграмма связей сущностей\n      user_defined_diagram: Пользовательская диаграмма\n      gantt_chart: Диаграмма Гантта\n      pie_chart: Круговая диаграмма\n    code:\n      text: Фрагмент кода\n      add_code: Добавить пример кода\n      form:\n        fields:\n          code:\n            label: Код\n            msg:\n              empty: Код не может быть пустым.\n          language:\n            label: Язык\n            placeholder: Автоматический выбор\n      btn_cancel: Отменить\n      btn_confirm: Добавить\n    formula:\n      text: Формула\n      options:\n        inline: Встроенная формула\n        block: Блочная формула\n    heading:\n      text: Заголовок\n      options:\n        h1: Заголовок 1\n        h2: Заголовок 2\n        h3: Заголовок 3\n        h4: Заголовок 4\n        h5: Заголовок 5\n        h6: Заголовок 6\n    help:\n      text: Помощь\n    hr:\n      text: Горизонтальная линия\n    image:\n      text: Изображение\n      add_image: Добавить изображение\n      tab_image: Загрузить изображение\n      form_image:\n        fields:\n          file:\n            label: Файл изображения\n            btn: Выбрать изображение\n            msg:\n              empty: Файл не может быть пустым.\n              only_image: Разрешены только изображения.\n              max_size: Размер файла не может превышать {{size}} МБ.\n          desc:\n            label: Описание\n      tab_url: URL изображения\n      form_url:\n        fields:\n          url:\n            label: URL изображения\n            msg:\n              empty: URL изображения не может быть пустым.\n          name:\n            label: Описание\n      btn_cancel: Отменить\n      btn_confirm: Добавь\n      uploading: Загрузка\n    indent:\n      text: Абзац\n    outdent:\n      text: Уменьшить отступ\n    italic:\n      text: Курсив\n    link:\n      text: Гиперссылка\n      add_link: Вставить гиперссылку\n      form:\n        fields:\n          url:\n            label: URL-адрес\n            msg:\n              empty: URL не может быть пустым.\n          name:\n            label: Описание\n      btn_cancel: Отменить\n      btn_confirm: Добавить\n    ordered_list:\n      text: Нумерованный список\n    unordered_list:\n      text: Маркированный список\n    table:\n      text: Таблица\n      heading: Заголовок\n      cell: Ячейка\n    file:\n      text: Прикрепить файлы\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Я закрываю этот пост как...\n    btn_cancel: Отменить\n    btn_submit: Сохранить\n    remark:\n      empty: Не может быть пустым.\n    msg:\n      empty: Пожалуйста, выбери причину.\n  report_modal:\n    flag_title: 'Причина жалобы:'\n    close_title: Я закрываю этот пост как...\n    review_question_title: Проверить вопрос\n    review_answer_title: Проверить ответ\n    review_comment_title: Просмотр комментариев\n    btn_cancel: Отмена\n    btn_submit: Сохранить\n    remark:\n      empty: Не может быть пустым.\n    msg:\n      empty: Пожалуйста, выбери причину.\n      not_a_url: Недопустимый формат URL.\n      url_not_match: URL адрес не соответствует текущему веб-сайту.\n  tag_modal:\n    title: Новый тег\n    form:\n      fields:\n        display_name:\n          label: Отображаемое имя\n          msg:\n            empty: Отображаемое название не может быть пустым.\n            range: Отображаемое имя до 35 символов.\n        slug_name:\n          label: URL-адрес тега\n          desc: URL-адрес тега длиной до 35 символов.\n          msg:\n            empty: URL slug не может быть пустым.\n            range: URL slug до 35 символов.\n            character: URL slug содержит недопустимый набор символов.\n        desc:\n          label: Описание\n        revision:\n          label: Версия\n        edit_summary:\n          label: Отредактировать сводку\n          placeholder: >-\n            Коротко опишите изменения (орфография, грамматики, улучшение формата)\n    btn_cancel: Отмена\n    btn_submit: Сохрнаить\n    btn_post: Создать новый тег\n  tag_info:\n    created_at: Создано\n    edited_at: Отредактировано\n    history: История\n    synonyms:\n      title: Синонимы\n      text: Следующие теги будут переназначены на\n      empty: Синонимы не найдены.\n      btn_add: Добавить синоним\n      btn_edit: Редактировать\n      btn_save: Сохранить\n    synonyms_text: Следующие теги будут переназначены на\n    delete:\n      title: Удалить этот тег\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Вы уверены, что хотите удалить?\n      close: Закрыть\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Изменить тег\n    default_reason: Правка тега\n    default_first_reason: Добавить метку\n    btn_save_edits: Сохранить изменения\n    btn_cancel: Отмена\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: сейчас\n    x_seconds_ago: \"{{count}}с назад\"\n    x_minutes_ago: \"{{count}}м назад\"\n    x_hours_ago: \"{{count}}ч назад\"\n    hour: часы\n    day: дней\n    hours: часов\n    days: дней\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: сердечко\n    smile: smile\n    frown: frown\n    btn_label: добавить или удалить реакции\n    undo_emoji: отменить реакцию {{ emoji }}\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Добавить комментарий\n    reply_to: Ответить на\n    btn_reply: Ответить\n    btn_edit: Редактирование\n    btn_delete: Удалить\n    btn_flag: Пожаловаться\n    btn_save_edits: Сохранить изменения\n    btn_cancel: Отменить\n    show_more: \"Еще {{count}} комментарий\"\n    tip_question: >-\n      Воспользуйтесь комментариями, чтобы запросить больше информации или предложить улучшения. Не отвечайте на вопросы в комментариях.\n    tip_answer: >-\n      Используйте комментарии для ответа другим пользователям или уведомления об изменениях. Если вы добавляете новую информацию, редактируйте ваше сообщение вместо комментариев.\n    tip_vote: Это добавляет кое-что полезное к сообщению\n  edit_answer:\n    title: Редактировать ответ\n    default_reason: Редактировать ответ\n    default_first_reason: Добавить ответ\n    form:\n      fields:\n        revision:\n          label: Пересмотр\n        answer:\n          label: Ответ\n          feedback:\n            characters: длина пароля должна составлять не менее 6 символов.\n        edit_summary:\n          label: Изменить краткое описание\n          placeholder: >-\n            Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование)\n    btn_save_edits: Сохранить изменения\n    btn_cancel: Отменить\n  tags:\n    title: Теги\n    sort_buttons:\n      popular: Популярное\n      name: Имя\n      newest: Последние\n    button_follow: Подписаться\n    button_following: Подписки\n    tag_label: вопросы\n    search_placeholder: Фильтр по названию тега\n    no_desc: Тег не имеет описания.\n    more: Подробнее\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Редактировать вопрос\n    default_reason: Редактировать вопрос\n    default_first_reason: Create question\n    similar_questions: Похожие вопросы\n    form:\n      fields:\n        revision:\n          label: Версия\n        title:\n          label: Заголовок\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Заголовок не может быть пустым.\n            range: Заголовок должен быть меньше 150 символов\n        body:\n          label: 'Вопрос:'\n          msg:\n            empty: Вопрос не может быть пустым.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Теги\n          msg:\n            empty: Теги не могут быть пустыми.\n        answer:\n          label: Ответ\n          msg:\n            empty: Ответ не может быть пустым.\n        edit_summary:\n          label: Изменить краткое описание\n          placeholder: >-\n            Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование)\n    btn_post_question: Задать вопрос\n    btn_save_edits: Сохранить изменения\n    answer_question: Ответить на свой собственный вопрос\n    post_question&answer: Опубликуйте свой вопрос и ответ\n  tag_selector:\n    add_btn: Тег\n    create_btn: новый тег\n    search_tag: Поиск тега\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Нет соответствующих тэгов\n    tag_required_text: Обязательный тег (хотя бы один)\n  header:\n    nav:\n      question: Вопросы\n      tag: Теги\n      user: Пользователи\n      badges: Значки\n      profile: Профиль\n      setting: Настройки\n      logout: Выйти\n      admin: Управление\n      review: Рецензия\n      bookmark: Закладки\n      moderation: Модерирование\n    search:\n      placeholder: Поиск\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Изменить\n    loading: загрузка...\n  pic_auth_code:\n    title: Капча\n    placeholder: Введите текст выше\n    msg:\n      empty: Капча не может быть пустой.\n  inactive:\n    first: >-\n      Вы почти закончили! Мы отправили письмо с активацией на адрес <bold>{{mail}}</bold>. Пожалуйста, следуйте инструкциям в письме, чтобы активировать свою учетную запись.\n    info: \"Если оно не пришло, проверьте свою папку со спамом.\"\n    another: >-\n      Мы отправили вам еще одно электронное письмо с активацией по адресу <bold>{{mail}}</bold>. Его получение может занять несколько минут; обязательно проверьте папку со спамом.\n    btn_name: Повторно отправить письмо с активацией\n    change_btn_name: Изменить email\n    msg:\n      empty: Не может быть пустым.\n    resend_email:\n      url_label: Вы уверены, что хотите повторно отправить письмо с активацией?\n      url_text: Вы также можете предоставить пользователю ссылку для активации выше.\n  login:\n    login_to_continue: Войдите, чтобы продолжить\n    info_sign: У вас нет аккаунта? <1>Зарегистрируйтесь</1>\n    info_login: Уже есть аккаунт? <1>Войти</1>\n    agreements: Регистрируясь, вы соглашаетесь с <1>политикой конфиденциальности</1> и <3>условиями обслуживания</3>.\n    forgot_pass: Забыли пароль?\n    name:\n      label: Имя пользователя\n      msg:\n        empty: Имя пользователя не должно быть пустым.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email адрес\n      msg:\n        empty: Адрес электронной почты не может быть пустым.\n    password:\n      label: Пароль\n      msg:\n        empty: Пароль не может быть пустым.\n        different: Введенные пароли не совпадают\n  account_forgot:\n    page_title: Забыли свой пароль\n    btn_name: Отправить мне письмо для восстановления пароля\n    send_success: >-\n      Если учетная запись соответствует <strong>{{mail}}</strong>, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль.\n    email:\n      label: Email адрес\n      msg:\n        empty: Адрес электронной почты не может быть пустым.\n  change_email:\n    btn_cancel: Отмена\n    btn_update: Сменить адрес email\n    send_success: >-\n      Если учетная запись соответствует <strong>{{mail}}</strong>, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль.\n    email:\n      label: Новый email\n      msg:\n        empty: Email не может быть пустым.\n  oauth:\n    connect: Связаться с {{ auth_name }}\n    remove: Удалить {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Добавьте адрес электронной почты для восстановления учетной записи.\n    btn_update: Сменить адрес email\n    email:\n      label: Электронная почта\n      msg:\n        empty: Адрес электронной почты не может быть пустым.\n    modal_title: Электронная почта уже существует.\n    modal_content: Этот адрес электронной почты уже зарегистрирован. Вы уверены, что хотите подключиться к существующей учетной записи?\n    modal_cancel: Изменить адрес электронной почты\n    modal_confirm: Подключение к существующей учетной записи\n  password_reset:\n    page_title: Сброс пароля\n    btn_name: Сбросить мой пароль\n    reset_success: >-\n      Вы успешно сменили свой пароль; вы будете перенаправлены на страницу входа в систему.\n    link_invalid: >-\n      Извините, эта ссылка для сброса пароля больше недействительна. Возможно, ваш пароль уже сброшен?\n    to_login: Перейдите на страницу входа в систему\n    password:\n      label: Пароль\n      msg:\n        empty: Пароль не может быть пустым.\n        length: Длина должна быть от 8 до 32\n        different: Введенные пароли не совпадают\n    password_confirm:\n      label: Подтвердите новый пароль\n  settings:\n    page_title: Настройки\n    goto_modify: Перейдите к изменению\n    nav:\n      profile: Профиль\n      notification: Уведомления\n      account: Учетная запись\n      interface: Интерфейс\n    profile:\n      heading: Профиль\n      btn_name: Сохранить\n      display_name:\n        label: Отображаемое имя\n        msg: Отображаемое имя не может быть пустым.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Имя пользователя\n        caption: Люди могут упоминать вас как \"@username\".\n        msg: Имя пользователя не может быть пустым.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Изображение профиля\n        gravatar: Gravatar\n        gravatar_text: Вы можете изменить изображение на\n        custom: Другой\n        custom_text: Вы можете загрузить свое изображение.\n        default: Системные\n        msg: Пожалуйста, загрузите аватар\n      bio:\n        label: Обо мне\n      website:\n        label: Сайт\n        placeholder: \"https://example.com\"\n        msg: Неправильный формат веб-сайта\n      location:\n        label: Местоположение\n        placeholder: \"Город, страна\"\n    notification:\n      heading: Уведомления по эл. почте\n      turn_on: Вкл.\n      inbox:\n        label: Email уведомления\n        description: Ответы на ваши вопросы, комментарии, приглашения и многое другое.\n      all_new_question:\n        label: Все новые вопросы\n        description: Получайте уведомления обо всех новых вопросах. До 50 вопросов в неделю.\n      all_new_question_for_following_tags:\n        label: Все новые вопросы для тегов из подписок\n        description: Получайте уведомления о новых вопросах по следующим тегам.\n    account:\n      heading: Учетная запись\n      change_email_btn: Изменить e-mail\n      change_pass_btn: Изменить пароль\n      change_email_info: >-\n        Мы отправили электронное письмо на этот адрес. Пожалуйста, следуйте инструкциям из письма.\n      email:\n        label: Email\n      new_email:\n        label: Новый email\n        msg: Новый email не может быть пустым.\n      pass:\n        label: Текущий пароль\n        msg: Пароль не может быть пустым.\n      password_title: Пароль\n      current_pass:\n        label: Текущий пароль\n        msg:\n          empty: Текущий пароль не может быть пустым.\n          length: Длина должна быть от 8 до 32.\n          different: Введенные пароли не совпадают.\n      new_pass:\n        label: Новый пароль\n      pass_confirm:\n        label: Подтвердите новый пароль\n    interface:\n      heading: Интерфейс\n      lang:\n        label: Язык интерфейса\n        text: Язык пользовательского интерфейса. Он изменится при обновлении страницы.\n    my_logins:\n      title: Мои логины\n      label: Войдите в систему или зарегистрируйтесь на этом сайте, используя эти учетные записи.\n      modal_title: Удаление логина\n      modal_content: Вы уверены, что хотите удалить этот логин из своей учетной записи?\n      modal_confirm_btn: Удалить\n      remove_success: Успешно удалено\n  toast:\n    update: успешное обновление\n    update_password: Пароль успешно изменен.\n    flag_success: Благодарим за отметку.\n    forbidden_operate_self: Запрещено работать с собой\n    review: Ваша версия будет отображаться после проверки.\n    sent_success: Отправлено успешно\n  related_question:\n    title: Related\n    answers: ответы\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Позвать на помощь\n    desc: Выберите людей, которые, по вашему мнению, могут знать ответ.\n    invite: Пригласил вас ответить\n    add: Добавить пользователей\n    search: Поиск людей\n  question_detail:\n    action: Действия\n    created: Created\n    Asked: Спросил(а)\n    asked: спросил(а)\n    update: Изменён\n    Edited: Edited\n    edit: отредактировал\n    commented: commented\n    Views: Просмотрен\n    Follow: Подписаться\n    Following: Подписки\n    follow_tip: Подпишитесь на этот вопрос для получения уведомлений\n    answered: отвеченные\n    closed_in: Закрыто в\n    show_exist: Показать существующий вопрос.\n    useful: Полезный\n    question_useful: Это полезно и понятно\n    question_un_useful: Это непонятно или не полезно\n    question_bookmark: Добавьте этот вопрос в закладки\n    answer_useful: Это полезно\n    answer_un_useful: Это бесполезно\n    answers:\n      title: Ответы\n      score: Оценка\n      newest: Последние\n      oldest: Oldest\n      btn_accept: Принять\n      btn_accepted: Принято\n    write_answer:\n      title: Ваш ответ\n      edit_answer: Редактировать мой существующий ответ\n      btn_name: Ответить\n      add_another_answer: Добавить другой ответ\n      confirm_title: Перейти к ответу\n      continue: Продолжить\n      confirm_info: >-\n        <p>Вы уверены, что хотите добавить другой ответ?</p><p>Вы можете использовать ссылку редактирования для уточнения и улучшения существующего ответа.</p>\n      empty: Ответ не может быть пустым.\n      characters: длина содержимого должна составлять не менее 6 символов.\n      tips:\n        header_1: Спасибо за ответ\n        li1_1: Пожалуйста, обязательно <strong>отвечайте на вопрос</strong>. Предоставьте подробности и поделитесь результатами своих исследований.\n        li1_2: Поддерживайте свои высказывания ссылками или личным опытом.\n        header_2: Но <strong>избегайте</strong> ...\n        li2_1: Просить о помощи, запрашивать уточнения или отвечать на другие ответы.\n    reopen:\n      confirm_btn: Снова открыть\n      title: Открыть повторно этот пост\n      content: Вы уверены, что хотите открыть заново?\n    list:\n      confirm_btn: Список\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Убрать из списка\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Закрепить сообщение\n      content: Вы уверены, что хотите закрепить глобально? Это сообщение появится вверху всех списков сообщений.\n      confirm_btn: Закрепить\n  delete:\n    title: Удалить сообщение\n    question: >-\n      Мы не рекомендуем <strong>удалять вопросы с ответами</strong>, поскольку это лишает будущих читателей этих знаний.</p><p>Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить?\n    answer_accepted: >-\n      Мы не рекомендуем <strong>удалять вопросы с ответами</strong>, поскольку это лишает будущих читателей этих знаний.</p><p>Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить?\n    other: Вы уверены, что хотите удалить?\n    tip_answer_deleted: Этот ответ был удален\n    undelete_title: Восстановить сообщение\n    undelete_desc: Вы уверены, что хотите отменить удаление?\n  btns:\n    confirm: Подтвердить\n    cancel: Отменить\n    edit: Редактировать\n    save: Сохранить\n    delete: Удалить\n    undelete: Отменить удаление\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Авторизоваться\n    signup: Регистрация\n    logout: Выйти\n    verify: Подтвердить\n    create: Create\n    approve: Одобрить\n    reject: Отклонить\n    skip: Пропустить\n    discard_draft: Удалить черновик\n    pinned: Закрепленный\n    all: Все\n    question: Вопрос\n    answer: Ответ\n    comment: Комментарий\n    refresh: Обновить\n    resend: Отправить повторно\n    deactivate: Отключить\n    active: Активные\n    suspend: Заблокировать\n    unsuspend: Разблокировать\n    close: Закрыть\n    reopen: Открыть повторно\n    ok: ОК\n    light: Светлая тема\n    dark: Темная тема\n    system_setting: Настройки системы\n    default: По умолчанию\n    reset: Сбросить\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Результаты поиска\n    keywords: Ключевые слова\n    options: Настройки\n    follow: Подписаться\n    following: Подписка\n    counts: \"Результатов: {{count}}\"\n    counts_loading: \"... Results\"\n    more: Ещё\n    sort_btns:\n      relevance: По релевантности\n      newest: Последние\n      active: Активные\n      score: Оценки\n      more: Больше\n    tips:\n      title: Советы по расширенному поиску\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> поиск по автору\"\n      answer: \"<1>ответов:0</1> вопросы без ответов\"\n      score: \"<1>score:3</1> записи с рейтингом 3+\"\n      question: \"<1>is:question</1> поиск по вопросам\"\n      is_answer: \"<1>ответ</1> поиск ответов\"\n    empty: Мы ничего не смогли найти. <br /> Попробуйте другие или менее специфичные ключевые слова.\n  share:\n    name: Поделиться\n    copy: Скопировать ссылку\n    via: Поделитесь постом через...\n    copied: Скопировано\n    facebook: Поделиться на Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Вы не можете проголосовать за свой собственный пост.\n  modal_confirm:\n    title: Ошибка...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Ваша новая учетная запись подтверждена; вы будете перенаправлены на главную страницу.\n    link: Перейти на главную\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Ваш адрес электронной почты был обновлен.\n    confirm_new_email_invalid: >-\n      Извините, эта ссылка для подтверждения больше недействительна. Возможно, ваш адрес электронной почты уже был изменен?\n  unsubscribe:\n    page_title: Отписаться\n    success_title: Вы успешно отписались от рассылки\n    success_desc: Вы были успешно удалены из этого списка подписчиков и больше не будете получать от нас никаких электронных писем.\n    link: Изменить настройки\n  question:\n    following_tags: Подписка на теги\n    edit: Редактировать\n    save: Сохранить\n    follow_tag_tip: Подпишитесь на теги, чтобы следить за интересующими темами.\n    hot_questions: Популярные вопросы\n    all_questions: Все вопросы\n    x_questions: \"{{ count }} вопросов\"\n    x_answers: \"{{ count }} ответов\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Вопросы\n    answers: Ответы\n    newest: Последние\n    active: Активные\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Оценка\n    unanswered: Без ответа\n    modified: изменён\n    answered: отвеченные\n    asked: спросил(а)\n    closed: закрытый\n    follow_a_tag: Следить за тегом\n    more: Подробнее\n  personal:\n    overview: Обзор\n    answers: Ответы\n    answer: ответ\n    questions: Вопросы\n    question: вопрос\n    bookmarks: Закладки\n    reputation: Репутация\n    comments: Комментарии\n    votes: Голоса\n    badges: Badges\n    newest: Последние\n    score: Оценки\n    edit_profile: Редактировать профиль\n    visited_x_days: \"Посещено {{ count }} дней\"\n    viewed: Просмотрен\n    joined: Присоединился\n    comma: \",\"\n    last_login: Просмотрен(-а)\n    about_me: О себе\n    about_me_empty: \"// Привет, Мир!\"\n    top_answers: Лучшие ответы\n    top_questions: Топ вопросов\n    stats: Статистика\n    list_empty: Сообщений не найдено.<br />Возможно, вы хотели бы выбрать другую вкладку?\n    content_empty: No posts found.\n    accepted: Принято\n    answered: отвеченные\n    asked: спросил\n    downvoted: проголосовано против\n    mod_short: MOD\n    mod_long: Модераторы\n    x_reputation: репутация\n    x_votes: полученные голоса\n    x_answers: ответы\n    x_questions: вопросы\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Следующий\n    done: Готово\n    config_yaml_error: Не удается создать файл config.yaml.\n    lang:\n      label: Пожалуйста, выберите язык\n    db_type:\n      label: База данных\n    db_username:\n      label: Имя пользователя\n      placeholder: root\n      msg: Имя пользователя не может быть пустым.\n    db_password:\n      label: Пароль\n      placeholder: root\n      msg: Пароль не может быть пустым.\n    db_host:\n      label: Сервер базы данных\n      placeholder: \"db:3306\"\n      msg: Сервер базы данных не может быть пустым.\n    db_name:\n      label: Название базы данных\n      placeholder: ответ\n      msg: Имя базы данных не может быть пустым.\n    db_file:\n      label: Файл базы данных\n      placeholder: /data/answer.db\n      msg: Файл базы данных не может быть пустым.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Создайте файл config.yaml\n      label: Файл config.yaml создан.\n      desc: >-\n        Вы можете создать файл <1>config.yaml</1> вручную в каталоге <1>/var/wwww/xxx/</1> и вставить в него следующий текст.\n      info: После этого нажмите на кнопку \"Далее\".\n    site_information: Информация о сайте\n    admin_account: Администратор\n    site_name:\n      label: Название сайта\n      msg: Название сайта не может быть пустым.\n      msg_max_length: Длина названия сайта должна составлять не более 30 символов.\n    site_url:\n      label: Адрес сайта\n      text: Адрес вашего сайта.\n      msg:\n        empty: URL-адрес сайта не может быть пустым.\n        incorrect: Неверный формат URL-адреса сайта.\n        max_length: Длина URL-адреса сайта должна составлять не более 512 символов.\n    contact_email:\n      label: Контактный адрес электронной почты\n      text: Адрес электронной почты контактного лица, ответственного за этот сайт.\n      msg:\n        empty: Контактный адрес электронной почты не может быть пустым.\n        incorrect: Некорректный формат контактного адреса электронной почты.\n    login_required:\n      label: Приватный\n      switch: Требуется авторизация\n      text: Только зарегистрированные пользователи могут получить доступ к этому сообществу.\n    admin_name:\n      label: Имя\n      msg: Имя не может быть пустым.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Пароль\n      text: >-\n        Этот пароль понадобится вам для входа в систему. Пожалуйста, сохраните его в надежном месте.\n      msg: Пароль не может быть пустым.\n      msg_min_length: Длина пароля должна составлять не менее 8 символов.\n      msg_max_length: Длина пароля должна составлять не более 32 символов.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: Вам понадобится этот адрес электронной почты для входа в систему.\n      msg:\n        empty: Адрес электронной почты не может быть пустым.\n        incorrect: Недопустимый формат e-mail адреса.\n    ready_title: Your site is ready\n    ready_desc: >-\n      Если вам когда-нибудь захочется изменить дополнительные настройки, посетите <1>раздел администратора</1>; найдите его в меню сайта.\n    good_luck: \"Получайте удовольствие и удачи!\"\n    warn_title: Предупреждение\n    warn_desc: >-\n      Файл <1>config.yaml</1> уже существует. Если вам нужно сбросить любой из элементов конфигурации в этом файле, пожалуйста, удалите его.\n    install_now: Вы можете попробовать <1>установить сейчас</1>.\n    installed: Уже установлено\n    installed_desc: >-\n      Похоже, вы уже установили. Для переустановки, пожалуйста, сначала очистите ваши старые таблицы базы данных.\n    db_failed: Ошибка подключения к базе данных\n    db_failed_desc: >-\n      Это означает, что информация о базе данных в вашем файле <1>config.yaml</1> неверна, либо не удалось установить контакт с сервером базы данных. Это может означать, что сервер базы данных вашего хоста недоступен.\n  counts:\n    views: просмотры\n    votes: голоса\n    answers: ответы\n    accepted: Принято\n  page_error:\n    http_error: Ошибка HTTP {{ code }}\n    desc_403: Нет прав доступа для просмотра этой страницы.\n    desc_404: К сожалению, эта страница не существует.\n    desc_50X: Сервер обнаружил ошибку и не смог выполнить ваш запрос.\n    back_home: Вернуться на главную страницу\n  page_maintenance:\n    desc: \"Мы выполняем техническое обслуживание, скоро вернемся.\"\n  nav_menus:\n    dashboard: Панель управления\n    contents: Содержимое\n    questions: Вопросы\n    answers: Ответы\n    users: Пользователи\n    badges: Badges\n    flags: Отметить\n    settings: Настройки\n    general: Основные\n    interface: Интерфейс\n    smtp: SMTP\n    branding: Фирменное оформление\n    legal: Правовая информация\n    write: Написать\n    terms: Terms\n    tos: Пользовательское Соглашение\n    privacy: Конфиденциальность\n    seo: SEO\n    customize: Настройки интерфейса\n    themes: Темы\n    login: Вход\n    privileges: Привилегии\n    plugins: Плагины\n    installed_plugins: Установленные плагины\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Добро пожаловать на {{site_name}}\n  user_center:\n    login: Вход\n    qrcode_login_tip: Пожалуйста, используйте {{ agentName }} для сканирования QR-кода и входа в систему.\n    login_failed_email_tip: Не удалось войти в систему, пожалуйста, разрешите этому приложению получить доступ к вашей электронной почте, прежде чем повторять попытку.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Администратор\n    dashboard:\n      title: Панель управления\n      welcome: Welcome to Admin!\n      site_statistics: Статистика сайта\n      questions: \"Вопросы:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Ответы:\"\n      comments: \"Комментарии:\"\n      votes: \"Голоса:\"\n      users: \"Пользователи:\"\n      flags: \"Жалобы:\"\n      reviews: \"Reviews:\"\n      site_health: Здоровье сайта\n      version: \"Версия:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Каталог загрузки:\"\n      run_mode: \"Режим приватности:\"\n      private: Приватный\n      public: Публичные\n      smtp: \"SMTP:\"\n      timezone: \"Часовой пояс:\"\n      system_info: Информация о системе\n      go_version: \"Версия GO:\"\n      database: \"База данных:\"\n      database_size: \"Размер базы данных:\"\n      storage_used: \"Использовано хранилища: \"\n      uptime: \"Время работы:\"\n      links: Ссылки\n      plugins: Плагины\n      github: GitHub\n      blog: Блог\n      contact: Контакты\n      forum: Форум\n      documents: Документы\n      feedback: Обратная связь\n      support: Поддержка\n      review: Обзор\n      config: Конфигурация\n      update_to: Обновление до\n      latest: Последние\n      check_failed: Проверка не удалась\n      \"yes\": \"Да\"\n      \"no\": \"Нет\"\n      not_allowed: Запрещено\n      allowed: Разрешено\n      enabled: Включено\n      disabled: Отключено\n      writable: Доступен для записи\n      not_writable: Не доступен для записи\n    flags:\n      title: Жалобы\n      pending: Ожидают\n      completed: Рассмотрены\n      flagged: Жалобы\n      flagged_type: Жалоба {{ type }}\n      created: Создано\n      action: Действие\n      review: На проверку\n    user_role_modal:\n      title: Изменить роль пользователя на...\n      btn_cancel: Отмена\n      btn_submit: Отправить\n    new_password_modal:\n      title: Задать новый пароль\n      form:\n        fields:\n          password:\n            label: Пароль\n            text: Сессия пользователя будет завершена и ему придется повторить вход.\n            msg: Длина пароля должна составлять от 8 до 32 символов.\n      btn_cancel: Отменить\n      btn_submit: Отправить\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Создание новых пользователей\n      form:\n        fields:\n          users:\n            label: Массовое добавление пользователей\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Разделите “name, email, password” запятыми. По одному пользователю в строке.\n            msg: \"Пожалуйста, введите адрес электронной почты пользователя, по одному на строку.\"\n          display_name:\n            label: Отображаемое имя\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Некорректный email.\n          password:\n            label: Пароль\n            msg: Длина пароля должна составлять от 8 до 32 символов.\n      btn_cancel: Отменить\n      btn_submit: Отправить\n    users:\n      title: Пользователи\n      name: Имя\n      email: Email\n      reputation: Репутация\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Статус\n      role: Роль\n      action: Действия\n      change: Изменить\n      all: Все\n      staff: Сотрудники\n      more: Ещё\n      inactive: Неактивные\n      suspended: Заблокированные\n      deleted: Удаленные\n      normal: Обычный\n      Moderator: Модератор\n      Admin: Администратор\n      User: Пользователь\n      filter:\n        placeholder: \"Фильтровать по имени, user:id\"\n      set_new_password: Задать новый пароль\n      edit_profile: Edit profile\n      change_status: Изменить статус\n      change_role: Изменить роль\n      show_logs: Показать логи\n      add_user: Добавить пользователя\n      deactivate_user:\n        title: Деактивировать пользователя\n        content: Неактивный пользователь должен будет повторно подтвердить свою электронную почту.\n      delete_user:\n        title: Удалить этого пользователя\n        content: Вы уверены, что хотите удалить этого пользователя? Это действие необратимо!\n        remove: Удалить контент пользователя (опционально)\n        label: Удалить все вопросы, ответы, комментарии и т.д.\n        text: Не устанавливайте этот флажок, если вы хотите удалить только учетную запись пользователя.\n      suspend_user:\n        title: Заблокировать этого пользователя\n        content: Заблокированный пользователь не сможет войти.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Вопросы\n      unlisted: Unlisted\n      post: Публикация\n      votes: Голоса\n      answers: Ответы\n      created: Создан\n      status: Статус\n      action: Действие\n      change: Изменить\n      pending: Ожидают\n      filter:\n        placeholder: \"Фильтровать по заголовку, question:id\"\n    answers:\n      page_title: Ответы\n      post: Публикация\n      votes: Голоса\n      created: Создан\n      status: Статус\n      action: Действие\n      change: Изменить\n      filter:\n        placeholder: \"Фильтровать по заголовку, answer:id\"\n    general:\n      page_title: Основные\n      name:\n        label: Название сайта\n        msg: Название сайта не может быть пустым.\n        text: \"Название сайта, используемое в теге title.\"\n      site_url:\n        label: URL-адрес сайта\n        msg: URL-адрес сайта не может быть пустым.\n        validate: Пожалуйста, введите корректный URL.\n        text: Адрес вашего сайта.\n      short_desc:\n        label: Краткое описание\n        msg: Краткое описание сайта не может быть пустым.\n        text: \"Краткое описание, используемое в теге заголовка на домашней странице.\"\n      desc:\n        label: Описание сайта\n        msg: Описание сайта не может быть пустым.\n        text: \"Опишите этот сайт одним предложением, как используется в теге meta description\"\n      contact_email:\n        label: Контактный адрес электронной почты\n        msg: Контактный адрес электронной почты не может быть пустым.\n        validate: Контактный адрес электронной почты не может быть пустым.\n        text: Адрес электронной почты контактного лица, ответственного за данный сайт.\n      check_update:\n        label: Обновления программного обеспечения\n        text: Автоматически проверять наличие обновлений\n    interface:\n      page_title: Интерфейс\n      language:\n        label: Язык интерфейса\n        msg: Язык интерфейса не может быть пустым.\n        text: Язык пользовательского интерфейса. Он изменится при обновлении страницы.\n      time_zone:\n        label: Часовой пояс\n        msg: Часовой пояс не может быть пустым.\n        text: Выберите город в том же часовом поясе, что и вы.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: С эл. почты\n        msg: Адрес электронной почты отправителя не может быть пустым.\n        text: Адрес электронной почты, с которого отправляются письма.\n      from_name:\n        label: Имя отправителя\n        msg: Имя пользователя не может быть пустым.\n        text: Имя, с которого отправляются электронные письма.\n      smtp_host:\n        label: Сервер SMTP\n        msg: Сервер SMTP не может быть пустым.\n        text: Ваш почтовый сервер.\n      encryption:\n        label: Шифрование\n        msg: Шифрование не может быть пустым.\n        text: Для большинства серверов рекомендуется использовать протокол SSL.\n        ssl: SSL\n        tls: TLS\n        none: Нет\n      smtp_port:\n        label: Порт SMTP\n        msg: Порт SMTP должен быть числом 1 ~ 65535.\n        text: Порт для вашего почтового сервера.\n      smtp_username:\n        label: Имя пользователя SMTP\n        msg: Имя пользователя SMTP не может быть пустым.\n      smtp_password:\n        label: Пароль SMTP\n        msg: Пароль SMTP не может быть пустым.\n      test_email_recipient:\n        label: Тестовые получатели электронной почты\n        text: Укажите адрес электронной почты, на который будут отправляться тестовые сообщения.\n        msg: Некорректный тестовый адрес электронной почты\n      smtp_authentication:\n        label: Включить авторизацию\n        title: Аутентификация SMTP\n        msg: Аутентификационные данные для SMTP не могут быть пустыми.\n        \"yes\": \"Да\"\n        \"no\": \"Нет\"\n    branding:\n      page_title: Фирменное оформление\n      logo:\n        label: Логотип\n        msg: Логотип не может быть пустым.\n        text: Изображение логотипа в левом верхнем углу вашего сайта. Используйте широкое прямоугольное изображение высотой 56 см с соотношением сторон более 3:1. Если оставить поле пустым, будет показан текст заголовка сайта.\n      mobile_logo:\n        label: Мобильный логотип\n        text: Логотип, используемый в мобильной версии вашего сайта. Используйте широкое прямоугольное изображение высотой 56. Если оставить пустым, будет использоваться изображение из настройки \"Логотип\".\n      square_icon:\n        label: Квадратный значок\n        msg: Square icon не может быть пустым.\n        text: Изображение, используемое в качестве основы для значков метаданных. В идеале должно быть больше 512x512.\n      favicon:\n        label: Иконка\n        text: Значок для вашего сайта. Для корректной работы через CDN он должен быть в формате png. Размер будет изменен до 32x32. Если оставить пустым, будет использоваться \"square icon\".\n    legal:\n      page_title: Правовая информация\n      terms_of_service:\n        label: Условия использования\n        text: \"Вы можете добавить содержимое условий предоставления услуг здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь.\"\n      privacy_policy:\n        label: Условия конфиденциальности\n        text: \"Вы можете добавить содержание политики конфиденциальности здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Каждый пользователь может написать только один ответ на каждый вопрос\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Рекомендованные теги\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Каждый новый вопрос должен иметь хотя бы один рекомендуемый тег.\"\n      reserved_tags:\n        label: Зарезервированные теги\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Постоянная ссылка\n        text: Пользовательские структуры URL-адресов могут улучшить удобство использования и обратную совместимость ваших ссылок.\n      robots:\n        label: robots.txt\n        text: Это приведет к необратимому переопределению всех связанных настроек сайта.\n    themes:\n      page_title: Темы\n      themes:\n        label: Темы\n        text: Выберите существующую тему.\n      color_scheme:\n        label: Цветовая схема\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Основной цвет\n        text: Измените цвета, используемые в ваших темах\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS и HTML\n      custom_css:\n        label: Пользовательский CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Нижняя панель\n        text: Это будет вставлено перед &lt;/body>.\n      sidebar:\n        label: Боковая панель\n        text: Это будет вставлено в боковую панель.\n    login:\n      page_title: Авторизоваться\n      membership:\n        title: Участие в сообществах\n        label: Разрешить новые регистрации\n        text: Отключите, чтобы никто не мог создать новую учетную запись.\n      email_registration:\n        title: Регистрация по электронной почте\n        label: Разрешить регистрацию по электронной почте\n        text: Отключите, чтобы предотвратить создание новой учетной записи через электронную почту.\n      allowed_email_domains:\n        title: Разрешенные домены электронной почты\n        text: Домены электронной почты, с которыми пользователи должны регистрировать аккаунты. Один домен на каждой строке. Игнорируется, если пусто.\n      private:\n        title: Приватный\n        label: Требуется авторизация\n        text: Только зарегистрированные пользователи могут получить доступ к этому сообществу.\n      password_login:\n        title: Вход в пароль\n        label: Разрешить вход по паролю\n        text: \"Предупреждение: При отключении, вы не сможете войти, если ранее не настроили другой способ входа.\"\n    installed_plugins:\n      title: Установленные плагины\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: Все\n        active: Активные\n        inactive: Неактивные\n        outdated: Устаревшие\n      plugins:\n        label: Плагины\n        text: Выберите существующий плагин.\n      name: Название\n      version: Версия\n      status: Статус\n      action: Действие\n      deactivate: Деактивировать\n      activate: Активировать\n      settings: Настройки\n    settings_users:\n      title: Пользователи\n      avatar:\n        label: Аватар по умолчанию\n        text: Для пользователей, у которых нет собственного пользовательского аватара.\n      gravatar_base_url:\n        label: Базовый URL Gravatar\n        text: URL базы API провайдера Gravatar. Игнорируется, если пусто.\n      profile_editable:\n        title: Настройки профилей\n      allow_update_display_name:\n        label: Разрешить пользователям изменять отображаемое имя\n      allow_update_username:\n        label: Разрешить пользователям изменять свой username\n      allow_update_avatar:\n        label: Разрешить пользователям изменять изображение своего профиля\n      allow_update_bio:\n        label: Разрешить пользователям изменять свои сведения в поле \"обо мне\"\n      allow_update_website:\n        label: Разрешить пользователям изменять свой веб-сайт\n      allow_update_location:\n        label: Разрешить пользователям изменять свое местоположение\n    privilege:\n      title: Привилегии\n      level:\n        label: Необходимый уровень репутации\n        text: Выберите количество репутации, необходимое для получения привилегий\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (опционально)\n    empty: не может быть пустым\n    invalid: недействителен\n    btn_submit: Сохранить\n    not_found_props: \"Требуемое свойство {{ key }} не найдено.\"\n    select: Select\n  page_review:\n    review: На проверку\n    proposed: предложенный\n    question_edit: Редактировать вопрос\n    answer_edit: Редактирование ответа\n    tag_edit: Редактирование тега\n    edit_summary: Редактирование краткого описания\n    edit_question: Редактирование вопроса\n    edit_answer: Редактирование ответа\n    edit_tag: Редактирование тега\n    empty: Нет задач для проверки.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Предложенные исправления\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: репутация\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: Восстановлен\n    deleted: Удаленные\n    downvote: бесполезный\n    upvote: оценить\n    accept: принять\n    cancelled: отменен\n    commented: прокомментированный\n    rollback: откатить\n    edited: отредактированный\n    answered: отвеченные\n    asked: asked\n    closed: закрытый\n    reopened: Открыт повторно\n    created: созданный\n    pin: закрепленный\n    unpin: незакреплённые\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Хронология\"\n    show_votes: \"Show votes\"\n    n_or_a: Недоступно\n    title_for_question: \"Хронология\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Дата и время\n    type: Тип\n    by: Автор\n    comment: Комментарий\n    no_data: \"Ничего не найдено.\"\n  users:\n    title: Пользователи\n    users_with_the_most_reputation: Пользователи с самой высокой репутацией на этой неделе\n    users_with_the_most_vote: Пользователи, которые больше всего проголосовали на этой неделе\n    staffs: Сотрудники нашего сообщества\n    reputation: репутация\n    votes: голоса\n  prompt:\n    leave_page: Вы уверены, что хотите покинуть страницу?\n    changes_not_save: Ваши изменения могут не быть сохранены.\n  draft:\n    discard_confirm: Вы уверены, что хотите отказаться от своего черновика?\n  messages:\n    post_deleted: Этот пост был удалён.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: Этот пост был закреплен.\n    post_unpin: Этот пост был откреплен.\n    post_hide_list: Это сообщение было скрыто из списка.\n    post_show_list: Этот пост был показан в списке.\n    post_reopen: Этот пост был вновь открыт.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/sk_SK.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Úspech.\n    unknown:\n      other: Neznáma chyba.\n    request_format_error:\n      other: Formát žiadosti nie je platný.\n    unauthorized_error:\n      other: Neoprávnené.\n    database_error:\n      other: Chyba dátového servera.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Edit\n    delete:\n      other: Delete\n    close:\n      other: Close\n    reopen:\n      other: Reopen\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: List\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Užívateľ\n      admin:\n        other: Správca\n      moderator:\n        other: Moderátor\n    description:\n      user:\n        other: Predvolené bez špeciálneho prístupu.\n      admin:\n        other: Má plnú moc a prístup ku stránke.\n      moderator:\n        other: Má prístup ku všetkým príspevkom okrem nastavenia správcu.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: E-mail\n  e_mail:\n    other: Email\n  password:\n    other: Heslo\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: E-mail a heslo sa nezhodujú.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: Svoje heslo upraviť.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: Nemôžete upraviť svoj stav.\n      email_or_password_wrong:\n        other: E-mail a heslo sa nezhodujú.\n    answer:\n      not_found:\n        other: Odpoveď sa nenašla.\n      cannot_deleted:\n        other: Žiadne povolenie na odstránenie.\n      cannot_update:\n        other: Žiadne povolenie na aktualizáciu.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Komentár nie je dovolené upravovať.\n      not_found:\n        other: Komentár sa nenašiel.\n      cannot_edit_after_deadline:\n        other: Čas na úpravu komentára bol príliš dlhý.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: E-mail už existuje.\n      need_to_be_verified:\n        other: E-mail by sa mal overiť.\n      verify_url_expired:\n        other: Platnosť overenej adresy URL e-mailu vypršala, pošlite e-mail znova.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Jazykový súbor sa nenašiel.\n    object:\n      captcha_verification_failed:\n        other: Captcha zle.\n      disallow_follow:\n        other: Nemáte dovolené sledovať.\n      disallow_vote:\n        other: Nemáte povolené hlasovať.\n      disallow_vote_your_self:\n        other: Nemôžete hlasovať za svoj vlastný príspevok.\n      not_found:\n        other: Objekt sa nenašiel.\n      verification_failed:\n        other: Overenie zlyhalo.\n      email_or_password_incorrect:\n        other: E-mail a heslo sa nezhodujú.\n      old_password_verification_failed:\n        other: Overenie starého hesla zlyhalo\n      new_password_same_as_previous_setting:\n        other: Nové heslo je rovnaké ako predchádzajúce.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: Tento príspevok bol odstránený.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Otázka sa nenašla.\n      cannot_deleted:\n        other: Žiadne povolenie na odstránenie.\n      cannot_close:\n        other: Žiadne povolenie na uzavretie.\n      cannot_update:\n        other: Žiadne povolenie na aktualizáciu.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Spracovanie prehľadu zlyhalo.\n      not_found:\n        other: Hlásenie sa nenašlo.\n    tag:\n      already_exist:\n        other: Značka už existuje.\n      not_found:\n        other: Značka sa nenašla.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Zadajte aspoň jednu požadovanú značku.\n      not_contain_synonym_tags:\n        other: Nemal by obsahovať synonymické značky.\n      cannot_update:\n        other: Žiadne povolenie na aktualizáciu.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: Synonymum aktuálnej značky nemôžete nastaviť ako samotnú.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Téma sa nenašla.\n    revision:\n      review_underway:\n        other: Momentálne nie je možné upravovať, vo fronte na kontrolu je verzia.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: E-mail a heslo sa nezhodujú.\n      not_found:\n        other: Používateľ nenájdený.\n      suspended:\n        other: Používateľ bol pozastavený.\n      username_invalid:\n        other: Používateľské meno je neplatné.\n      username_duplicate:\n        other: Používateľské meno sa už používa.\n      set_avatar:\n        other: Nastavenie avatara zlyhalo.\n      cannot_update_your_role:\n        other: Svoju rolu nemôžete zmeniť.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read Config zlyhal\n    database:\n      connection_failed:\n        other: Databázové pripojenie zlyhalo\n      create_table_failed:\n        other: Vytvorenie tabuľky zlyhalo\n    install:\n      create_config_failed:\n        other: Nie je možné vytvoriť súbor config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Nepodporovaný formát súboru.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: nevyžiadaná pošta\n        desc:\n          other: Táto otázka už bola položená a už má odpoveď.\n      guideline:\n        name:\n          other: dôvod špecifický pre komunitu\n        desc:\n          other: Táto otázka nespĺňa pokyny pre komunitu.\n      multiple:\n        name:\n          other: potrebuje podrobnosti alebo jasnosť\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: niečo iné\n        desc:\n          other: Tento príspevok vyžaduje iný dôvod, ktorý nie je uvedený vyššie.\n    operation_type:\n      asked:\n        other: požiadaný\n      answered:\n        other: zodpovedaný\n      modified:\n        other: upravený\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: aktualizovaná otázka\n      answer_the_question:\n        other: zodpovedaná otázka\n      update_answer:\n        other: aktualizovaná odpoveď\n      accept_answer:\n        other: prijatá odpoveď\n      comment_question:\n        other: komentovaná otázka\n      comment_answer:\n        other: komentovaná odpoveď\n      reply_to_you:\n        other: odpovedal vám\n      mention_you:\n        other: spomenul vás\n      your_question_is_closed:\n        other: Vaša otázka bola uzavretá\n      your_question_was_deleted:\n        other: Vaša otázka bola odstránená\n      your_answer_was_deleted:\n        other: Vaša odpoveď bola odstránená\n      your_comment_was_deleted:\n        other: Váš komentár bol odstránený\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Ako formátovať\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Predch\n    next: Ďalšie\n  page_title:\n    question: Otázka\n    questions: Otázky\n    tag: Značka\n    tags: Značky\n    tag_wiki: značka wiki\n    create_tag: Vytvoriť štítok\n    edit_tag: Upraviť značku\n    ask_a_question: Create Question\n    edit_question: Úpraviť otázku\n    edit_answer: Úpraviť odpoveť\n    search: Vyhľadávanie\n    posts_containing: Príspevky obsahujúce\n    settings: Nastavenie\n    notifications: Oznámenia\n    login: Prihlásiť sa\n    sign_up: Prihlásiť Se\n    account_recovery: Obnovenie účtu\n    account_activation: Aktivácia účtu\n    confirm_email: Potvrď e-mail\n    account_suspended: Účet pozastavený\n    admin: Administrátor\n    change_email: Upraviť e-mail\n    install: Odpoveď Inštalácia\n    upgrade: Answer Upgrade\n    maintenance: Údržba webových stránok\n    users: Užívatelia\n    oauth_callback: Processing\n    http_404: HTTP chyba 404\n    http_50X: HTTP chyba 403\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Oznámenia\n    inbox: Doručená pošta\n    achievement: Úspechy\n    new_alerts: New alerts\n    all_read: Označiť všetko ako prečítané\n    show_more: Zobraziť viac\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Váš účet bol pozastavený\n    until_time: \"Váš účet bol pozastavený do {{ time }}.\"\n    forever: Tento používateľ bol navždy pozastavený.\n    end: Nespĺňate pokyny pre komunitu.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Silný\n    chart:\n      text: Rebríček\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Ganttov diagram\n      pie_chart: Koláčový graf\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Kód\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Jazyk\n            placeholder: Automatic detection\n      btn_cancel: Zrušiť\n      btn_confirm: Pridať\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Pomoc\n    hr:\n      text: Horizontal rule\n    image:\n      text: Obrázok\n      add_image: Pridať obrázok\n      tab_image: Nahrať obrázok\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Vyberte obrázok\n            msg:\n              empty: Názov súboru nemôže byť prázdny.\n              only_image: Povolené sú iba obrázkové súbory.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Popis\n      tab_url: URL obrázka\n      form_url:\n        fields:\n          url:\n            label: URL obrázka\n            msg:\n              empty: URL obrázka nemôže byť prázdna.\n          name:\n            label: Description\n      btn_cancel: Zrušiť\n      btn_confirm: Pridať\n      uploading: Nahráva sa\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hypertextový odkaz\n      add_link: Pridať hypertextový odkaz\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL adresa nemôže byť prázdna.\n          name:\n            label: Popis\n      btn_cancel: Zrušiť\n      btn_confirm: Pridať\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Bunka\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: Tento príspevok uzatváram ako...\n    btn_cancel: Zrušiť\n    btn_submit: Potvrdiť\n    remark:\n      empty: Nemôže byť prázdny.\n    msg:\n      empty: Vyberte dôvod.\n  report_modal:\n    flag_title: Nahlasujem nahlásenie tohto príspevku ako...\n    close_title: Tento príspevok zatváram ako ...\n    review_question_title: Kontrola otázky\n    review_answer_title: Kontrola odpovede\n    review_comment_title: Kontrola komentára\n    btn_cancel: Zrušiť\n    btn_submit: Potvrdiť\n    remark:\n      empty: Nemôže byť prázdny.\n    msg:\n      empty: Vyberte dôvod.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Vytvorte novú značku\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Zobrazovaný názov nemôže byť prázdny.\n            range: Zobrazovaný názov do 35 znakov.\n        slug_name:\n          label: URL slug\n          desc: URL slug do 35 znakov.\n          msg:\n            empty: URL slug nemôže byť prázdny.\n            range: URL slug do 35 znakov.\n            character: URL slug obsahuje nepovolenú znakovú sadu.\n        desc:\n          label: Opis\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Zrušiť\n    btn_submit: Potvrdiť\n    btn_post: Post new tag\n  tag_info:\n    created_at: Vytvorená\n    edited_at: Upravená\n    history: História\n    synonyms:\n      title: Synonymá\n      text: Nasledujúce značky budú premapované na\n      empty: Nenašli sa žiadne synonymá.\n      btn_add: Pridajte synonymum\n      btn_edit: Upraviť\n      btn_save: Uložiť\n    synonyms_text: Nasledujúce značky budú premapované na\n    delete:\n      title: Odstrániť túto značku\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Naozaj chcete odstrániť?\n      close: Zavrieť\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Upraviť značku\n    default_reason: Upraviť značku\n    default_first_reason: Add tag\n    btn_save_edits: Uložiť úpravy\n    btn_cancel: Zrušiť\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [o] HH:mm\"\n    now: teraz\n    x_seconds_ago: \"pred {{count}}s\"\n    x_minutes_ago: \"pred {{count}}m\"\n    x_hours_ago: \"pred {{count}}h\"\n    hour: hodina\n    day: deň\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Pridať komentár\n    reply_to: Odpovedať\n    btn_reply: Odpovedať\n    btn_edit: Upraviť\n    btn_delete: Zmazať\n    btn_flag: Vlajka\n    btn_save_edits: Uložiť zmeny\n    btn_cancel: Zrušiť\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Uprav odpoveď\n    default_reason: Uprav odpoveď\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revízia\n        answer:\n          label: Odpoveď\n          feedback:\n            characters: Obsah musí mať dĺžku najmenej 6 znakov.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie)\n    btn_save_edits: Uložiť úpravy\n    btn_cancel: Zrušiť\n  tags:\n    title: Značky\n    sort_buttons:\n      popular: Populárne\n      name: názov\n      newest: Newest\n    button_follow: Sledovať\n    button_following: Sledované\n    tag_label: otázky\n    search_placeholder: Filtrujte podľa názvu značky\n    no_desc: Značka nemá popis.\n    more: Viac\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Upraviť otázku\n    default_reason: Upraviť otázku\n    default_first_reason: Create question\n    similar_questions: Podobné otázky\n    form:\n      fields:\n        revision:\n          label: Revízia\n        title:\n          label: Názov\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Názov nemôže byť prázdny.\n            range: Názov do 150 znakov\n        body:\n          label: Telo\n          msg:\n            empty: Telo nemôže byť prázdne.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Značky --\n          msg:\n            empty: Štítky nemôžu byť prázdne.\n        answer:\n          label: Odpoveď\n          msg:\n            empty: Odpoveď nemôže byť prázdna.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie)\n    btn_post_question: Uverejnite svoju otázku\n    btn_save_edits: Uložiť úpravy\n    answer_question: Odpovedzte na svoju vlastnú otázku\n    post_question&answer: Uverejnite svoju otázku a odpoveď\n  tag_selector:\n    add_btn: Pridať značku\n    create_btn: Vytvoriť novú značku\n    search_tag: Vyhľadať značku --\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Nezodpovedajú žiadne značky\n    tag_required_text: Povinný štítok (aspoň jeden)\n  header:\n    nav:\n      question: Otázky\n      tag: Značky\n      user: Užívatelia\n      badges: Badges\n      profile: Profil\n      setting: Nastavenia\n      logout: Odhlásiť sa\n      admin: Správca\n      review: Preskúmanie\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Vyhľadávanie\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Zmena\n    loading: načítavanie...\n  pic_auth_code:\n    title: captcha\n    placeholder: Zadajte vyššie uvedený text\n    msg:\n      empty: Captcha nemôže byť prázdna.\n  inactive:\n    first: >-\n      Ste takmer na konci! Poslali sme Vám aktivačný mail na adresu <bold>{{mail}}</bold>. K aktivácií účtu postupujte prosím podľa pokynov v e-maily.\n    info: \"Ak neprichádza, skontrolujte priečinok spamu.\"\n    another: >-\n      Poslali sme vám ďalší aktivačný e-mail na adresu <bold>{{mail}}</bold>. Môže to trvať niekoľko minút; Nezabudnite skontrolovať priečinok spamu.\n    btn_name: Opätovne odoslať aktivačný e-mail\n    change_btn_name: Zmeniť e-mail\n    msg:\n      empty: Nemôže byť prázdny.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Pre pokračovanie sa prihláste\n    info_sign: Nemáte účet? <1>Sign up</1>\n    info_login: Máte už účet? <1>Log in</1>\n    agreements: Registráciou súhlasíte s <1>zásadami ochrany osobných údajov</1> a <3>podmienkami služby</3>.\n    forgot_pass: Zabudli ste heslo?\n    name:\n      label: Prihlasovacie meno\n      msg:\n        empty: Prihlasovacie meno nemôže byť prázdne.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail nemôže byť prázdny.\n    password:\n      label: Heslo\n      msg:\n        empty: Heslo nemôže byť prázdne.\n        different: Heslá zadané na oboch stranách sú nekonzistentné\n  account_forgot:\n    page_title: Zabudli ste heslo\n    btn_name: Pošlite mi e-mail na obnovenie\n    send_success: >-\n      Ak sa účet zhoduje s <strong>{{mail}}</strong>, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo.\n    email:\n      label: E-mail\n      msg:\n        empty: E-mail nemôže byť prázdny.\n  change_email:\n    btn_cancel: Zrušiť\n    btn_update: Aktualizovať e-mailovú adresu\n    send_success: >-\n      Ak sa účet zhoduje s <strong>{{mail}}</strong>, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo.\n    email:\n      label: New email\n      msg:\n        empty: E-mail nemôže byť prázdny.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Resetovanie hesla\n    btn_name: Obnoviť heslo\n    reset_success: >-\n      Úspešne ste zmenili svoje heslo; Budete presmerovaný na prihlásenie.\n    link_invalid: >-\n      Ospravedlňujeme sa, tento odkaz na obnovenie hesla už nie je platný. Možno už došlo k resetovaniu vašho hesla?\n    to_login: Continue to log in page\n    password:\n      label: Heslo\n      msg:\n        empty: Heslo nemôže byť prázdne.\n        length: Dĺžka musí byť medzi 8 a 32\n        different: Heslá zadané na oboch stranách sú nekonzistentné\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Nastavenia\n    goto_modify: Go to modify\n    nav:\n      profile: Profil\n      notification: Oznámenia\n      account: Účet\n      interface: Rozhranie\n    profile:\n      heading: Profil\n      btn_name: Uložiť\n      display_name:\n        label: Display name\n        msg: Zobrazované meno nemôže byť prázdne.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Užívateľské meno\n        caption: Ľudia vás môžu spomenúť ako „@používateľské meno“.\n        msg: Užívateľské meno nemôže byť prázdne.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Vlastný\n        custom_text: Môžete nahrať svoj obrázok.\n        default: Systém\n        msg: Nahrajte avatara prosím\n      bio:\n        label: About me\n      website:\n        label: Webová stránka\n        placeholder: \"https://priklad.com\"\n        msg: Nesprávny formát webovej stránky\n      location:\n        label: Poloha\n        placeholder: \"Mesto, Krajina\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Účet\n      change_email_btn: Zmeniť e-mail\n      change_pass_btn: Zmeniť heslo\n      change_email_info: >-\n        Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Heslo\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: Dĺžka musí byť medzi 8 a 32.\n          different: Dve zadané heslá sa nezhodujú.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Rozhranie\n      lang:\n        label: Interface language\n        text: Jazyk používateľského rozhrania. Zmení sa pri obnove stránky.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: aktualizácia úspešna\n    update_password: Heslo bolo úspešne zmenené.\n    flag_success: Ďakujeme za nahlásenie.\n    forbidden_operate_self: Zakázané operovať seba\n    review: Vaša revízia sa zobrazí po preskúmaní.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: odpovede\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Select people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Opýtané\n    asked: opýtané\n    update: Aktualizované\n    Edited: Edited\n    edit: upravené\n    commented: commented\n    Views: Videné\n    Follow: Sledovať\n    Following: Sledované\n    follow_tip: Follow this question to receive notifications\n    answered: zodpovedaný\n    closed_in: Uzatvorené\n    show_exist: Ukázať existujúcu otázku.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Odpovede\n      score: Skóre\n      newest: Najnovšie\n      oldest: Oldest\n      btn_accept: Súhlasiť\n      btn_accepted: Prijaté\n    write_answer:\n      title: Vaša odpoveď\n      edit_answer: Edit my existing answer\n      btn_name: Pošlite svoju odpoveď\n      add_another_answer: Pridajte ďalšiu odpoveď\n      confirm_title: Pokračovať v odpovedi\n      continue: Pokračovať\n      confirm_info: >-\n        <p>Ste si istí, že chcete pridať ďalšiu odpoveď?</p><p>Mohli by ste namiesto toho použiť úpravu na vylepšenie svojej už existujúcej odpovede.</p>\n      empty: Odpoveď nemôže byť prázdna.\n      characters: Minimálna dĺžka obsahu musí byť 6 znakov.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Znovu otvoriť tento príspevok\n      content: Ste si istý, že ho chcete znovu otvoriť?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Odstrániť tento príspevok\n    question: >-\n      Neodporúčame <strong> mazanie otázok s odpoveďmi</strong> pretože týmto oberáte budúcich čitateľov o tieto vedomostí.</p><p> Opakované mazanie zodpovedaných otázok môže mať za následok zablokovanie možnosti kladenia otázok z vášho účtu. Ste si istí, že chcete otázku odstrániť?\n    answer_accepted: >-\n      <p>Neodporúčame <strong>odstránenie akceptovanej odpovede</strong> pretože týmto oberáte budúcich čitateľov o tieto vedomostí.</p>Opakované mazanie akceptovaných odpovedí môže mať za následok zablokovanie možnosti odpovedať z vášho účtu. Ste si istí, že chcete odstrániť odpoveď?\n    other: Ste si istí, že ju chcete odstrániť?\n    tip_answer_deleted: Táto odpoveď bola odstránená\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Potvrdiť\n    cancel: Zrušiť\n    edit: Edit\n    save: Uložiť\n    delete: Vymazať\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Prihlásiť sa\n    signup: Registrovať sa\n    logout: Odhlásiť sa\n    verify: Preveriť\n    create: Create\n    approve: Schváliť\n    reject: Odmietnuť\n    skip: Preskočiť\n    discard_draft: Zahodiť koncept\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Výsledky vyhľadávania\n    keywords: Kľúčové slová\n    options: možnosti\n    follow: Sledovať\n    following: Sledované\n    counts: \"{{count}} výsledky\"\n    counts_loading: \"... Results\"\n    more: Viac\n    sort_btns:\n      relevance: Relevantnosť\n      newest: Najnovšie\n      active: Aktívne\n      score: Skóre\n      more: Viac\n    tips:\n      title: Tipy na pokročilé vyhľadávanie\n      tag: \"<1>[tag]</1> hľadať v rámci značky\"\n      user: \"<1>user:username</1> hľadať podľa autora\"\n      answer: \"<1>answers:0</1> nezodpovedané otázky\"\n      score: \"<1>score:3</1> Príspevky so skóre 3+\"\n      question: \"<1>is:question</1> hľadať otázky\"\n      is_answer: \"<1>is:answer</1> hľadať odpovede\"\n    empty: Nemohli sme nič nájsť. <br /> Vyskúšajte iné alebo menej špecifické kľúčové slová.\n  share:\n    name: Zdieľať\n    copy: Skopírovať odkaz\n    via: Zdieľajte príspevok cez...\n    copied: Skopírované\n    facebook: Zdieľať na Facebooku\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Chyba...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Váš nový účet je potvrdený; Budete presmerovaný na domovskú stránku.\n    link: Pokračovať na domovskú stránku\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Váš e-mail bol aktualizovaný.\n    confirm_new_email_invalid: >-\n      Ospravedlňujeme sa, tento potvrdzovací odkaz už nie je platný. Váš e-mail je už môžno zmenený.\n  unsubscribe:\n    page_title: Zrušiť odber\n    success_title: Úspešne zrušenie odberu\n    success_desc: Boli ste úspešne odstránený zo zoznamu odoberateľov a nebudete od nás dostávať žiadne ďalšie e-maily.\n    link: Zmeniť nastavenia\n  question:\n    following_tags: Nasledujúce značky\n    edit: Upraviť\n    save: Uložiť\n    follow_tag_tip: Postupujte podľa značiek a upravte si zoznam otázok.\n    hot_questions: Najlepšie otázky\n    all_questions: Všetky otázky\n    x_questions: \"{{ count }} otázky/otázok\"\n    x_answers: \"{{ count }} odpovede/odpovedí\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Otázky\n    answers: Odpovede\n    newest: Najnovšie\n    active: Aktívne\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Skóre\n    unanswered: Nezodpovedané\n    modified: upravené\n    answered: zodpovedané\n    asked: opýtané\n    closed: uzatvorené\n    follow_a_tag: Postupujte podľa značky\n    more: Viac\n  personal:\n    overview: Prehľad\n    answers: Odpovede\n    answer: odpoveď\n    questions: Otázky\n    question: otázka\n    bookmarks: Záložky\n    reputation: Reputácia\n    comments: Komentáre\n    votes: Hlasovanie\n    badges: Badges\n    newest: Najnovšie\n    score: Skóre\n    edit_profile: Edit profile\n    visited_x_days: \"Navštívené {{ count }} dni\"\n    viewed: Videné\n    joined: Pripojené\n    comma: \",\"\n    last_login: Videné\n    about_me: O mne\n    about_me_empty: \"// Dobrý deň, svet!\"\n    top_answers: Najlepšie odpovede\n    top_questions: Najlepšie otázky\n    stats: Štatistiky\n    list_empty: Nenašli sa žiadne príspevky.<br />Možno by ste chceli vybrať inú kartu?\n    content_empty: No posts found.\n    accepted: Prijaté\n    answered: zodpovedané\n    asked: opýtané\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderátori\n    x_reputation: reputácia\n    x_votes: prijatých hlasov\n    x_answers: odpovede\n    x_questions: otázky\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Ďalšie\n    done: Hotový\n    config_yaml_error: Nie je možné vytvoriť súbor config.yaml.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Užívateľské meno\n      placeholder: super užívateľ\n      msg: Užívateľské meno nemôže byť prázdne.\n    db_password:\n      label: Heslo\n      placeholder: super užívateľ\n      msg: Heslo nemôže byť prázdne.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: odpoveď\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Vytvoriť config.yaml\n      label: Vytvorený súbor Config.yaml.\n      desc: >-\n        Súbor <1>config.yaml</1> môžete vytvoriť manuálne v adresári <1>/var/www/xxx/</1> a vložiť doň nasledujúci text.\n      info: Potom, čo ste to urobili, kliknite na tlačidlo „Ďalej“.\n    site_information: Informácie o stránke\n    admin_account: Správca\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: URL stránky\n      text: Adresa vašej stránky.\n      msg:\n        empty: URL stránky nemôže byť prázdny.\n        incorrect: Nesprávny formát adresy URL.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: E-mailová adresa kontaktu zodpovedného za túto stránku.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Meno\n      msg: Meno nemôže byť prázdne.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Heslo\n      text: >-\n        Na prihlásenie budete potrebovať toto heslo. Uložte si ho na bezpečné miesto.\n      msg: Heslo nemôže byť prázdne.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: E-mail\n      text: Na prihlásenie budete potrebovať tento e-mail.\n      msg:\n        empty: E-mail nemôže byť prázdny.\n        incorrect: Nesprávny formát e-mailu\n    ready_title: Your site is ready\n    ready_desc: >-\n      Ak niekedy budete chcieť zmeniť viac nastavení, navštívte stránku <1>admin section</1>; Nájdete ju v ponuke stránok.\n    good_luck: \"„Bavte sa a veľa šťastia!“\"\n    warn_title: Upozornenie\n    warn_desc: >-\n      Súbor <1>config.yaml</1> už existuje. Ak potrebujete resetovať niektorú z konfiguračných položiek v tomto súbore, najskôr ju odstráňte.\n    install_now: Môžete skúsiť <1>installing now</1>.\n    installed: Už nainštalované\n    installed_desc: >-\n      Zdá sa, že ste už aplikáciu answer nainštalovali. Ak chcete aplikáciu preinštalovať, najprv vymažte staré tabuľky z databázy.\n    db_failed: Databázové pripojenie zlyhalo\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: názory\n    votes: hlasy\n    answers: odpovede\n    accepted: prijaté\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"Prebieha údržba, čoskoro sa vrátime.\"\n  nav_menus:\n    dashboard: Nástenka\n    contents: Obsah\n    questions: Otázky\n    answers: Odpovede\n    users: Užívatelia\n    badges: Badges\n    flags: Vlajky\n    settings: Nastavenia\n    general: Všeobecné\n    interface: Rozhranie\n    smtp: SMTP\n    branding: Budovanie značky\n    legal: legálne\n    write: písať\n    terms: Terms\n    tos: Podmienky služby\n    privacy: Súkromie\n    seo: SEO\n    customize: Prispôsobiť\n    themes: Témy\n    login: Prihlásiť sa\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Administrátor\n    dashboard:\n      title: Nástenka\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Otázky:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Odpovede:\"\n      comments: \"Komentáre:\"\n      votes: \"Hlasy:\"\n      users: \"Users:\"\n      flags: \"Vlajky:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Verzia:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Časové pásmo:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Použité úložisko:\"\n      uptime: \"Doba prevádzky:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Dokumenty\n      feedback: Spätná väzba\n      support: Podpora\n      review: Preskúmanie\n      config: Konfigurácia\n      update_to: Aktualizovať na\n      latest: Posledné\n      check_failed: Skontrolovať zlyhanie\n      \"yes\": \"Áno\"\n      \"no\": \"Nie\"\n      not_allowed: Nepovolené\n      allowed: Povolené\n      enabled: Povolené\n      disabled: Zablokované\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Vlajky\n      pending: Prebiehajúce\n      completed: Dokončené\n      flagged: Označené\n      flagged_type: Flagged {{ type }}\n      created: Vytvorené\n      action: Akcia\n      review: Preskúmanie\n    user_role_modal:\n      title: Zmeňte rolu používateľa na...\n      btn_cancel: Zrušiť\n      btn_submit: Odovzdať\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Používatelia\n      name: Meno\n      email: E-mail\n      reputation: Reputácia\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Stav\n      role: Rola\n      action: Akcia\n      change: Zmena\n      all: Všetko\n      staff: Personál\n      more: More\n      inactive: Neaktívne\n      suspended: Pozastavené\n      deleted: Vymazané\n      normal: Normálné\n      Moderator: Moderátor\n      Admin: Správca\n      User: Používateľ\n      filter:\n        placeholder: \"Filter podľa mena, používateľ: ID\"\n      set_new_password: Nastaviť nové heslo\n      edit_profile: Edit profile\n      change_status: Zmentiť stavu\n      change_role: Zmeniť rolu\n      show_logs: Zobraziť protokoly\n      add_user: Pridať používateľa\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Otázky\n      unlisted: Unlisted\n      post: poslané\n      votes: Hlasy\n      answers: Odpovede\n      created: Vytvorené\n      status: Stav\n      action: Akcia\n      change: Zmena\n      pending: Pending\n      filter:\n        placeholder: \"Filter podľa názvu, otázka:id\"\n    answers:\n      page_title: Odpovede\n      post: Poslané\n      votes: Hlasy\n      created: Vytvorené\n      status: Stav\n      action: Akcia\n      change: Zmena\n      filter:\n        placeholder: \"Filter podľa názvu, odpoveď:id\"\n    general:\n      page_title: Všeobecné\n      name:\n        label: Site name\n        msg: Názov stránky nemôže byť prázdny.\n        text: \"Názov tejto lokality, ako sa používa v značke názvu.\"\n      site_url:\n        label: URL stránky\n        msg: Adresa Url stránky nemôže byť prázdna.\n        validate: Prosím uveďte platnú webovú adresu.\n        text: Adresa vašej stránky.\n      short_desc:\n        label: Short site description\n        msg: Krátky popis stránky nemôže byť prázdny.\n        text: \"Krátky popis, ako sa používa v značke názvu na domovskej stránke.\"\n      desc:\n        label: Site description\n        msg: Popis stránky nemôže byť prázdny.\n        text: \"Opíšte túto stránku jednou vetou, ako sa používa v značke meta description.\"\n      contact_email:\n        label: Contact email\n        msg: Kontaktný e-mail nemôže byť prázdny.\n        validate: Kontaktný e-mail je neplatný.\n        text: E-mailová adresa kontaktu zodpovedného za túto stránku.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Rozhranie\n      language:\n        label: Interface language\n        msg: Jazyk rozhrania nemôže byť prázdny.\n        text: Jazyk používateľského rozhrania. Zmení sa, keď stránku obnovíte.\n      time_zone:\n        label: Časové pásmo\n        msg: Časové pásmo nemôže byť prázdne.\n        text: Vyberte si mesto v rovnakom časovom pásme ako vy.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: Z e-mailu nemôže byť prázdne.\n        text: E-mailová adresa, z ktorej sa odosielajú e-maily.\n      from_name:\n        label: From name\n        msg: Názov od nemôže byť prázdny.\n        text: Meno, z ktorého sa odosielajú e-maily.\n      smtp_host:\n        label: SMTP host\n        msg: Hostiteľ SMTP nemôže byť prázdny.\n        text: Váš mailový server.\n      encryption:\n        label: Šifrovanie\n        msg: Šifrovanie nemôže byť prázdne.\n        text: Pre väčšinu serverov je SSL odporúčaná možnosť.\n        ssl: SSL\n        tls: TLS\n        none: Žiadne\n      smtp_port:\n        label: SMTP port\n        msg: Port SMTP musí byť číslo 1 ~ 65535.\n        text: Port na váš poštový server.\n      smtp_username:\n        label: SMTP username\n        msg: Používateľské meno SMTP nemôže byť prázdne.\n      smtp_password:\n        label: SMTP password\n        msg: Heslo SMTP nemôže byť prázdne.\n      test_email_recipient:\n        label: Test email recipients\n        text: Zadajte e-mailovú adresu, na ktorú sa budú odosielať testy.\n        msg: Príjemcovia testovacieho e-mailu sú neplatní\n      smtp_authentication:\n        label: Povoliť autentifikáciu\n        title: SMTP authentication\n        msg: Overenie SMTP nemôže byť prázdne.\n        \"yes\": \"Áno\"\n        \"no\": \"Nie\"\n    branding:\n      page_title: Budovanie značky\n      logo:\n        label: Logo\n        msg: Logo nemôže byť prázdne.\n        text: Obrázok loga v ľavej hornej časti vašej stránky. Použite široký obdĺžnikový obrázok s výškou 56 a pomerom strán väčším ako 3:1. Ak ho ponecháte prázdne, zobrazí sa text názvu stránky.\n      mobile_logo:\n        label: Mobile logo\n        text: Logo použité na mobilnej verzii vášho webu. Použite široký obdĺžnikový obrázok s výškou 56. Ak pole ponecháte prázdne, použije sa obrázok z nastavenia „logo“.\n      square_icon:\n        label: Square icon\n        msg: Ikona štvorca nemôže byť prázdna.\n        text: Obrázok použitý ako základ pre ikony metadát. V ideálnom prípade by mal byť väčšií ako 512 x 512.\n      favicon:\n        label: favicon\n        text: Favicon pre váš web. Ak chcete správne fungovať cez CDN, musí to byť png. Veľkosť sa zmení na 32 x 32. Ak zostane prázdne, použije sa „štvorcová ikona“.\n    legal:\n      page_title: Legálne\n      terms_of_service:\n        label: Terms of service\n        text: \"Tu môžete pridať obsah zmluvných podmienok. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"Tu môžete pridať obsah zásad ochrany osobných údajov. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Každá nová otázka musí mať aspoň jedenu odporúčaciu značku.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: trvalý odkaz\n        text: Vlastné štruktúry URL môžu zlepšiť použiteľnosť a doprednú kompatibilitu vašich odkazov.\n      robots:\n        label: robots.txt\n        text: Toto natrvalo prepíše všetky nastavenia súvisiace so stránkou.\n    themes:\n      page_title: Témy\n      themes:\n        label: Témy\n        text: Vyberte existujúcu tému.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Upraviť farby používané vašími motívmi\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS a HTML\n      custom_css:\n        label: Vlastné CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Hlavička\n        text: >\n\n      footer:\n        label: Päta\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Prihlásenie\n      membership:\n        title: Členstvo\n        label: Povoliť nové registrácie\n        text: Vypnúť, aby sa zabránilo vytvorenie nového účtu hocikým.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Súkromné\n        label: Vyžaduje sa prihlásenie\n        text: Do tejto komunity majú prístup iba prihlásení používatelia\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (voliteľné)\n    empty: nemôže byť prázdne\n    invalid: je neplatné\n    btn_submit: Uložiť\n    not_found_props: \"Požadovaná vlastnosť {{ key }} nebola nájdená.\"\n    select: Select\n  page_review:\n    review: Preskúmanie\n    proposed: navrhované\n    question_edit: Úprava otázky\n    answer_edit: Úprava odpovede\n    tag_edit: Úprava značky\n    edit_summary: Upraviť súhrn\n    edit_question: Upraviť otázku\n    edit_answer: Upraviť odpoveď\n    edit_tag: Upraviť značku\n    empty: Nezostali žiadne úlohy kontroly.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: zrušené zmazanie\n    deleted: vymazané\n    downvote: hlasovať proti\n    upvote: hlasovať za\n    accept: akceptované\n    cancelled: zrušené\n    commented: komentované\n    rollback: Návrat\n    edited: zmenené\n    answered: odpovedané\n    asked: spýtané\n    closed: uzavreté\n    reopened: znovu otvorené\n    created: vytvorené\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"História pre\"\n    tag_title: \"Časová os pre\"\n    show_votes: \"Zobraziť hlasy\"\n    n_or_a: N/A\n    title_for_question: \"Časová os pre\"\n    title_for_answer: \"Časová os odpovede na {{ title }} od {{ author }}\"\n    title_for_tag: \"Časová os pre značku\"\n    datetime: Dátum a čas\n    type: Typ\n    by: Od\n    comment: Komentár\n    no_data: \"Nič sa nám nepodarilo nájsť.\"\n  users:\n    title: Použivatelia\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Zamestnanci našej komunity\n    reputation: reputácia\n    votes: hlasy\n  prompt:\n    leave_page: Ste si istý, že chcete opustiť stránku?\n    changes_not_save: Vaše zmeny nemusia byť uložené.\n  draft:\n    discard_confirm: Naozaj chcete zahodiť svoj koncept?\n  messages:\n    post_deleted: Tento príspevok bol odstránený.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/sq_AL.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: \"Success.\"\n    unknown:\n      other: \"Unknown error.\"\n    request_format_error:\n      other: \"Request format is not valid.\"\n    unauthorized_error:\n      other: \"Unauthorized.\"\n    database_error:\n      other: \"Data server error.\"\n  role:\n    name:\n      user:\n        other: \"User\"\n      admin:\n        other: \"Admin\"\n      moderator:\n        other: \"Moderator\"\n    description:\n      user:\n        other: \"Default with no special access.\"\n      admin:\n        other: \"Have the full power to access the site.\"\n      moderator:\n        other: \"Has access to all posts except admin settings.\"\n  email:\n    other: \"Email\"\n  password:\n    other: \"Password\"\n  email_or_password_wrong_error:\n    other: \"Email and password do not match.\"\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: \"Answer do not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    comment:\n      edit_without_permission:\n        other: \"Comment are not allowed to edit.\"\n      not_found:\n        other: \"Comment not found.\"\n    email:\n      duplicate:\n        other: \"Email already exists.\"\n      need_to_be_verified:\n        other: \"Email should be verified.\"\n      verify_url_expired:\n        other: \"Email verified URL has expired, please resend the email.\"\n    lang:\n      not_found:\n        other: \"Language file not found.\"\n    object:\n      captcha_verification_failed:\n        other: \"Captcha wrong.\"\n      disallow_follow:\n        other: \"You are not allowed to follow.\"\n      disallow_vote:\n        other: \"You are not allowed to vote.\"\n      disallow_vote_your_self:\n        other: \"You can't vote for your own post.\"\n      not_found:\n        other: \"Object not found.\"\n      verification_failed:\n        other: \"Verification failed.\"\n      email_or_password_incorrect:\n        other: \"Email and password do not match.\"\n      old_password_verification_failed:\n        other: \"The old password verification failed\"\n      new_password_same_as_previous_setting:\n        other: \"The new password is the same as the previous one.\"\n    question:\n      not_found:\n        other: \"Question not found.\"\n      cannot_deleted:\n        other: \"No permission to delete.\"\n      cannot_close:\n        other: \"No permission to close.\"\n      cannot_update:\n        other: \"No permission to update.\"\n    rank:\n      fail_to_meet_the_condition:\n        other: \"Rank fail to meet the condition.\"\n    report:\n      handle_failed:\n        other: \"Report handle failed.\"\n      not_found:\n        other: \"Report not found.\"\n    tag:\n      not_found:\n        other: \"Tag not found.\"\n      recommend_tag_not_found:\n        other: \"Recommend Tag is not exist.\"\n      recommend_tag_enter:\n        other: \"Please enter at least one required tag.\"\n      not_contain_synonym_tags:\n        other: \"Should not contain synonym tags.\"\n      cannot_update:\n        other: \"No permission to update.\"\n      cannot_set_synonym_as_itself:\n        other: \"You cannot set the synonym of the current tag as itself.\"\n    smtp:\n      config_from_name_cannot_be_email:\n        other: \"The From Name cannot be a email address.\"\n    theme:\n      not_found:\n        other: \"Theme not found.\"\n    revision:\n      review_underway:\n        other: \"Can't edit currently, there is a version in the review queue.\"\n      no_permission:\n        other: \"No permission to Revision.\"\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: \"User not found.\"\n      suspended:\n        other: \"User has been suspended.\"\n      username_invalid:\n        other: \"Username is invalid.\"\n      username_duplicate:\n        other: \"Username is already in use.\"\n      set_avatar:\n        other: \"Avatar set failed.\"\n      cannot_update_your_role:\n        other: \"You cannot modify your role.\"\n      not_allowed_registration:\n        other: \"Currently the site is not open for registration\"\n    config:\n      read_config_failed:\n        other: \"Read config failed\"\n    database:\n      connection_failed:\n        other: \"Database connection failed\"\n      create_table_failed:\n        other: \"Create table failed\"\n    install:\n      create_config_failed:\n        other: \"Can't create the config.yaml file.\"\n  report:\n    spam:\n      name:\n        other: \"spam\"\n      desc:\n        other: \"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"\n    rude:\n      name:\n        other: \"rude or abusive\"\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    duplicate:\n      name:\n        other: \"a duplicate\"\n      desc:\n        other: \"This question has been asked before and already has an answer.\"\n    not_answer:\n      name:\n        other: \"not an answer\"\n      desc:\n        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.\"\n    not_need:\n      name:\n        other: \"no longer needed\"\n      desc:\n        other: \"This comment is outdated, conversational or not relevant to this post.\"\n    other:\n      name:\n        other: \"something else\"\n      desc:\n        other: \"This post requires staff attention for another reason not listed above.\"\n  question:\n    close:\n      duplicate:\n        name:\n          other: \"spam\"\n        desc:\n          other: \"This question has been asked before and already has an answer.\"\n      guideline:\n        name:\n          other: \"a community-specific reason\"\n        desc:\n          other: \"This question doesn't meet a community guideline.\"\n      multiple:\n        name:\n          other: \"needs details or clarity\"\n        desc:\n          other: \"This question currently includes multiple questions in one. It should focus on one problem only.\"\n      other:\n        name:\n          other: \"something else\"\n        desc:\n          other: \"This post requires another reason not listed above.\"\n    operation_type:\n      asked:\n        other: \"asked\"\n      answered:\n        other: \"answered\"\n      modified:\n        other: \"modified\"\n  notification:\n    action:\n      update_question:\n        other: \"updated question\"\n      answer_the_question:\n        other: \"answered question\"\n      update_answer:\n        other: \"updated answer\"\n      accept_answer:\n        other: \"accepted answer\"\n      comment_question:\n        other: \"commented question\"\n      comment_answer:\n        other: \"commented answer\"\n      reply_to_you:\n        other: \"replied to you\"\n      mention_you:\n        other: \"mentioned you\"\n      your_question_is_closed:\n        other: \"Your question has been closed\"\n      your_question_was_deleted:\n        other: \"Your question has been deleted\"\n      your_answer_was_deleted:\n        other: \"Your answer has been deleted\"\n      your_comment_was_deleted:\n        other: \"Your comment has been deleted\"\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comment\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to Answer\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name up to 30 characters\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username up to 30 characters\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to Answer\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: \"After you've done that, click “Next” button.\"\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: display_name must be at 2 - 30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8 - 32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/sr_SP.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n#The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Success.\n    unknown:\n      other: Unknown error.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  email:\n    other: Email\n  password:\n    other: Password\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    admin:\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n    question:\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n    rank:\n      fail_to_meet_the_condition:\n        other: Rank fail to meet the condition.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend Tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The From Name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to Revision.\n    user:\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n  report:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude:\n      name:\n        other: rude or abusive\n      desc:\n        other: A reasonable person would find this content inappropriate for respectful discourse.\n    duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n    not_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\n    not_need:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    other:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n#The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <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>\n  pagination:\n    prev: Prev\n    next: Next\n  page_title:\n    question: Question\n    questions: Questions\n    tag: Tag\n    tags: Tags\n    tag_wiki: tag wiki\n    edit_tag: Edit Tag\n    ask_a_question: Add Question\n    edit_question: Edit Question\n    edit_answer: Edit Answer\n    search: Search\n    posts_containing: Posts containing\n    settings: Settings\n    notifications: Notifications\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n  notifications:\n    title: Notifications\n    inbox: Inbox\n    achievement: Achievements\n    all_read: Mark all as read\n    show_more: Show more\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language (optional)\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal Rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image File\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed 4 MB.\n          desc:\n            label: Description (optional)\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description (optional)\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered List\n    unordered_list:\n      text: Bulleted List\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display Name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL Slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description (optional)\n    btn_cancel: Cancel\n    btn_submit: Submit\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      content: >-\n        <p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>\n      content2: Are you sure you wish to delete?\n      close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    form:\n      fields:\n        revision:\n          label: Revision\n        display_name:\n          label: Display Name\n        slug_name:\n          label: URL Slug\n          info: URL slug up to 35 characters.\n        desc:\n          label: Description\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: Show more comments\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n  ask:\n    title: Add Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: Be specific and imagine you're asking a question to another person\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit Summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: \"Describe what your question is about, at least one tag is required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n    search:\n      placeholder: Search\n  footer:\n    build_on: >-\n      Built on <1> Answer </1>- the open-source software that powers Q&A communities.<br />Made with love © {{cc}}.\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n  login:\n    page_title: Welcome to {{site_name}}\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"A-Z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    page_title: Welcome to {{site_name}}\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New Email\n      msg:\n        empty: Email cannot be empty.\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm New Password\n  settings:\n    page_title: Settings\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display Name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile Image\n        gravatar: Gravatar\n        gravatar_text: You can change image on <1>gravatar.com</1>\n        custom: Custom\n        btn_refresh: Refresh\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About Me (optional)\n      website:\n        label: Website (optional)\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location (optional)\n        placeholder: \"City, Country\"\n    notification:\n      heading: Notifications\n      email:\n        label: Email Notifications\n        radio: \"Answers to your questions, comments, and more\"\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current Password\n        msg:\n          empty: Current Password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New Password\n      pass_confirm:\n        label: Confirm New Password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface Language\n        text: User interface language. It will change when you refresh the page.\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n  related_question:\n    title: Related Questions\n    btn: Add question\n    answers: answers\n  question_detail:\n    Asked: Asked\n    asked: asked\n    update: Modified\n    edit: edited\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n    reopen:\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n      success: This post has been reopened\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_question_deleted: This post has been deleted\n    tip_answer_deleted: This answer has been deleted\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    save: Save\n    delete: Delete\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    add_question: Add question\n    approve: Approve\n    reject: Reject\n    skip: Skip\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post\n  modal_confirm:\n    title: Error...\n  account_result:\n    page_title: Welcome to {{site_name}}\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    invalid: >-\n      Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    newest: Newest\n    score: Score\n    edit_profile: Edit Profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    upvote: upvote\n    downvote: downvote\n    mod_short: Mod\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please Choose a Language\n    db_type:\n      label: Database Engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database Host\n      placeholder: \"db:3306\"\n      msg: Database Host cannot be empty.\n    db_name:\n      label: Database Name\n      placeholder: answer\n      msg: Database Name cannot be empty.\n    db_file:\n      label: Database File\n      placeholder: /data/answer.db\n      msg: Database File cannot be empty.\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site Name\n      msg: Site Name cannot be empty.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n    contact_email:\n      label: Contact Email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact Email cannot be empty.\n        incorrect: Contact Email incorrect format.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_404:\n    desc: \"Unfortunately, this page doesn't exist.\"\n    back_home: Back to homepage\n  page_50X:\n    desc: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    css-html: CSS/HTML\n    login: Login\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site Statistics\n      questions: \"Questions:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      active_users: \"Active users:\"\n      flags: \"Flags:\"\n      site_health_status: Site Health Status\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      uploading_files: \"Uploading files:\"\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System Info\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      answer_links: Answer Links\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      created: Created\n      action: Action\n      review: Review\n    change_modal:\n      title: Change user status to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n      normal_name: normal\n      normal_desc: A normal user can ask and answer questions.\n      suspended_name: suspended\n      suspended_desc: A suspended user can't log in.\n      deleted_name: deleted\n      deleted_desc: \"Delete profile, authentication associations.\"\n      inactive_name: inactive\n      inactive_desc: An inactive user must re-validate their email.\n      confirm_title: Delete this user\n      confirm_content: Are you sure you want to delete this user? This is permanent!\n      confirm_btn: Delete\n      msg:\n        empty: Please select a reason.\n    status_modal:\n      title: \"Change {{ type }} status to...\"\n      normal_name: normal\n      normal_desc: A normal post available to everyone.\n      closed_name: closed\n      closed_desc: \"A closed question can't answer, but still can edit, vote and comment.\"\n      deleted_name: deleted\n      deleted_desc: All reputation gained and lost will be restored.\n      btn_cancel: Cancel\n      btn_submit: Submit\n      btn_next: Next\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created Time\n      delete_at: Deleted Time\n      suspend_at: Suspended Time\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      new_password_modal:\n        title: Set new password\n        form:\n          fields:\n            password:\n              label: Password\n              text: The user will be logged out and need to login again.\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n      user_modal:\n        title: Add new user\n        form:\n          fields:\n            display_name:\n              label: Display Name\n              msg: Display name must be 2-30 characters in length.\n            email:\n              label: Email\n              msg: Email is not valid.\n            password:\n              label: Password\n              msg: Password must be at 8-32 characters in length.\n        btn_cancel: Cancel\n        btn_submit: Submit\n    questions:\n      page_title: Questions\n      normal: Normal\n      closed: Closed\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      normal: Normal\n      deleted: Deleted\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site Name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short Site Description (optional)\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site Description (optional)\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact Email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n    interface:\n      page_title: Interface\n      logo:\n        label: Logo (optional)\n        msg: Site logo cannot be empty.\n        text: You can upload your image or <1>reset</1> it to the site title text.\n      theme:\n        label: Theme\n        msg: Theme cannot be empty.\n        text: Select an existing theme.\n      language:\n        label: Interface Language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From Email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From Name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP Host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        none: None\n      smtp_port:\n        label: SMTP Port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP Username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP Password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test Email Recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP Authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo (optional)\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile Logo (optional)\n        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.\n      square_icon:\n        label: Square Icon (optional)\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon (optional)\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of Service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy Policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n    write:\n      page_title: Write\n      recommend_tags:\n        label: Recommend Tags\n        text: \"Please input tag slug above, one tag per line.\"\n      required_tag:\n        title: Required Tag\n        label: Set recommend tag as required\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved Tags\n        text: \"Reserved tags can only be added to a post by moderator.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      navbar_style:\n        label: Navbar Style\n        text: Select an existing theme.\n      primary_color:\n        label: Primary Color\n        text: Modify the colors used by your themes\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: This will insert as <link>\n      head:\n        label: Head\n        text: This will insert before </head>\n      header:\n        label: Header\n        text: This will insert after <body>\n      footer:\n        label: Footer\n        text: This will insert before </html>.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n  form:\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores\n    users_with_the_most_vote: Users who voted the most\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n"
  },
  {
    "path": "i18n/sv_SE.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Åtgärden lyckades\n    unknown:\n      other: Okänt fel.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Access saknas\n    database_error:\n      other: Data server error.\n    forbidden_error:\n      other: Förbjudet.\n    duplicate_request_error:\n      other: Dubblett inlämning.\n  action:\n    report:\n      other: Flagga\n    edit:\n      other: Redigera\n    delete:\n      other: Radera\n    close:\n      other: Stäng\n    reopen:\n      other: Öppna igen\n    forbidden_error:\n      other: Förbjudet.\n    pin:\n      other: Fäst\n    hide:\n      other: Göm\n    unpin:\n      other: Lossa\n    show:\n      other: Lista\n    invite_someone_to_answer:\n      other: Redigera\n    undelete:\n      other: Återskapa\n    merge:\n      other: Sammanfoga\n  role:\n    name:\n      user:\n        other: Användare\n      admin:\n        other: Administratör\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Normal tillgång\n      admin:\n        other: Full kontroll över webbplatsen\n      moderator:\n        other: Tillgång till allt utom administratörsinställningar\n  privilege:\n    level_1:\n      description:\n        other: Nivå 1\n    level_2:\n      description:\n        other: Nivå 2\n    level_3:\n      description:\n        other: Nivå 3\n    level_custom:\n      description:\n        other: Anpassad nivå\n    rank_question_add_label:\n      other: Ställ en fråga\n    rank_answer_add_label:\n      other: Skriv ett svar\n    rank_comment_add_label:\n      other: Skriv en kommentar\n    rank_report_add_label:\n      other: Flagga\n    rank_comment_vote_up_label:\n      other: Bra kommentar\n    rank_link_url_limit_label:\n      other: Mer än 2 länkar samtidigt\n    rank_question_vote_up_label:\n      other: Bra fråga\n    rank_answer_vote_up_label:\n      other: Bra svar\n    rank_question_vote_down_label:\n      other: Dålig fråga\n    rank_answer_vote_down_label:\n      other: Dåligt svar\n    rank_invite_someone_to_answer_label:\n      other: Bjud in någon att svara\n    rank_tag_add_label:\n      other: Skapa ny tagg\n    rank_tag_edit_label:\n      other: Beskriv etiketten (behöver granskas)\n    rank_question_edit_label:\n      other: Editera annans fråga (behöver granskas)\n    rank_answer_edit_label:\n      other: Editera annans svar (behöver granskas)\n    rank_question_edit_without_review_label:\n      other: Editera annans fråga utan granskning\n    rank_answer_edit_without_review_label:\n      other: Editera annans svar utan granskning\n    rank_question_audit_label:\n      other: Granska ändringar av fråga\n    rank_answer_audit_label:\n      other: Granska ändringar av svar\n    rank_tag_audit_label:\n      other: Granska ändringar av etikett\n    rank_tag_edit_without_review_label:\n      other: Ändra etikett-beskrivningen utan granskning\n    rank_tag_synonym_label:\n      other: Hantera etikett-synonymer\n  email:\n    other: E-post\n  e_mail:\n    other: E-post\n  password:\n    other: Lösenord\n  pass:\n    other: Lösenord\n  old_pass:\n    other: Nuvarande lösenord\n  original_text:\n    other: Detta inlägg\n  email_or_password_wrong_error:\n    other: Fel e-post eller lösenord\n  error:\n    common:\n      invalid_url:\n        other: Ogiltig URL.\n      status_invalid:\n        other: Ogiltig status.\n    password:\n      space_invalid:\n        other: Lösenordet får inte innehålla mellanslag.\n    admin:\n      cannot_update_their_password:\n        other: Du får inte ändra ditt lösenord.\n      cannot_edit_their_profile:\n        other: Du får inte ändra din profil.\n      cannot_modify_self_status:\n        other: Du får inte ändra din status.\n      email_or_password_wrong:\n        other: Fel e-post eller lösenord. \n    answer:\n      not_found:\n        other: Svar hittades inte.\n      cannot_deleted:\n        other: Radering tillåts inte.\n      cannot_update:\n        other: No permission to update.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Kommentarsfältet får inte vara tomt.\n    email:\n      duplicate:\n        other: E-postadressen finns redan.\n      need_to_be_verified:\n        other: E-postadressen ska vara verifierad.\n      verify_url_expired:\n        other: Länken för att verifiera e-postadressen har gått ut. Vänligen skicka igen. \n      illegal_email_domain_error:\n        other: E-post från den domänen tillåts inte. Vänligen använt en annan. \n    lang:\n      not_found:\n        other: Språkfilen hittas inte. \n    object:\n      captcha_verification_failed:\n        other: Fel Captcha.\n      disallow_follow:\n        other: Du tillåts inte följa.\n      disallow_vote:\n        other: Du tillåts inte rösta.\n      disallow_vote_your_self:\n        other: Du får inte rösta på ditt eget inlägg.\n      not_found:\n        other: Objektet hittas inte.\n      verification_failed:\n        other: Verifiering misslyckades.\n      email_or_password_incorrect:\n        other: Fel e-postadress eller lösenord.\n      old_password_verification_failed:\n        other: Den gamla verifieringen av lösenordet misslyckades.\n      new_password_same_as_previous_setting:\n        other: Det nya lösenordet är samma som det förra.\n      already_deleted:\n        other: Det här inlägget har raderats.\n    meta:\n      object_not_found:\n        other: Meta-objekt hittas inte.\n    question:\n      already_deleted:\n        other: Det här inlägget har raderats.\n      under_review:\n        other: Ditt inlägg väntar på granskning. Det kommer att publiceras så snart det har blivit godkänt.\n      not_found:\n        other: .\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Tabellen kunde inte skapas.\n    install:\n      create_config_failed:\n        other: Filen config.yaml kan inte skapas.\n    upload:\n      unsupported_file_format:\n        other: Filformatet tillåts inte.\n    site_info:\n      config_not_found:\n        other: Webbplats inställningarna hittar inte.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Din fråga har raderats\n      your_answer_was_deleted:\n        other: Ditt svar har raderats\n      your_comment_was_deleted:\n        other: Din kommentar har raderats\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Bekräfta din nya e-postadress\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Ny fråga: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Bekräfta ditt nya konto\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Prev\n    next: Nästa\n  page_title:\n    question: Fråga\n    questions: Frågor\n    tag: Tagg\n    tags: Taggar\n    tag_wiki: tag wiki\n    create_tag: Create Tag\n    edit_tag: Edit Tag\n    ask_a_question: Create Question\n    edit_question: Redigera fråga\n    edit_answer: Redigera svar\n    search: Sök\n    posts_containing: Posts containing\n    settings: Inställningar\n    notifications: Notifications\n    login: Logga in\n    sign_up: Registrera dig\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: Account Suspended\n    admin: Admin\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Användare\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Logga ut\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Notifications\n    inbox: Inkorg\n    achievement: Achievements\n    new_alerts: New alerts\n    all_read: Markera alla som lästa\n    show_more: Visa mer\n    someone: Someone\n    inbox_type:\n      all: Alla\n      posts: Inlägg\n      invites: Inbjudningar\n      votes: Röster\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Kontakta oss\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Kod\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Språk\n            placeholder: Automatic detection\n      btn_cancel: Avbryt\n      btn_confirm: Lägg till\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Hjälp\n    hr:\n      text: Horizontal rule\n    image:\n      text: Bild\n      add_image: Lägg till bild\n      tab_image: Ladda upp bild\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Beskrivning\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Beskrivning\n      btn_cancel: Avbryt\n      btn_confirm: Lägg till\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Beskrivning\n      btn_cancel: Avbryt\n      btn_confirm: Lägg till\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Tabell\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Avbryt\n    btn_submit: Skicka\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Avbryt\n    btn_submit: Skicka\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Visningsnamn\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Beskrivning\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Avbryt\n    btn_submit: Skicka\n    btn_post: Post new tag\n  tag_info:\n    created_at: Skapad\n    edited_at: Edited\n    history: Historik\n    synonyms:\n      title: Synonymer\n      text: The following tags will be remapped to\n      empty: Inga synonymer hittades.\n      btn_add: Lägg till en synonym\n      btn_edit: Redigera\n      btn_save: Spara\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Stäng\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Lägg till tagg\n    btn_save_edits: Save edits\n    btn_cancel: Avbryt\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: nu\n    x_seconds_ago: \"{{count}} s sedan\"\n    x_minutes_ago: \"{{count}} m sedan\"\n    x_hours_ago: \"{{count}} t sedan\"\n    hour: timme\n    day: dag\n    hours: timmar\n    days: dagar\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Lägg till kommentar\n    reply_to: Reply to\n    btn_reply: Svara\n    btn_edit: Redigera\n    btn_delete: Radera\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Avbryt\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Avbryt\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Namn\n      newest: Newest\n    button_follow: Följ\n    button_following: Följer\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Användare\n      badges: Badges\n      profile: Profil\n      setting: Inställningar\n      logout: Logga ut\n      admin: Admin\n      review: Review\n      bookmark: Bokmärken\n      moderation: Moderation\n    search:\n      placeholder: Sök\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Ändra\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Logga in för att fortsätta\n    info_sign: Har du inget konto? <1>Registrera dig</1>\n    info_login: Har du redan ett konto? <1>Logga in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Glömt lösenord?\n    name:\n      label: Namn\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: E-postadress\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Lösenord\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Glömt ditt lösenord\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: E-postadress\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Avbryt\n    btn_update: Uppdatera e-postadress\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Ny e-postadress\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Uppdatera e-postadress\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Återställ mitt lösenord\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Lösenord\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Bekräfta nytt lösenord\n  settings:\n    page_title: Inställningar\n    goto_modify: Go to modify\n    nav:\n      profile: Profil\n      notification: Notifications\n      account: Konto\n      interface: Interface\n    profile:\n      heading: Profil\n      btn_name: Spara\n      display_name:\n        label: Visningsnamn\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Användarnamn\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profilbild\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: Du kan ladda upp din bild.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: Om mig\n      website:\n        label: Webbplats\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"Stad, Land\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Konto\n      change_email_btn: Change email\n      change_pass_btn: Ändra lösenord\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Ny e-postadress\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Nuvarande lösenord\n        msg: Password cannot be empty.\n      password_title: Lösenord\n      current_pass:\n        label: Nuvarande lösenord\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: Nytt lösenord\n      pass_confirm:\n        label: Bekräfta nytt lösenord\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Bjud in personer som du tror kan svara.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Ditt svar\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Fortsätt\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Fäst\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Är du säker på att du vill radera?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Bekräfta\n    cancel: Avbryt\n    edit: Redigera\n    save: Spara\n    delete: Radera\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Logga in\n    signup: Registrera dig\n    logout: Logga ut\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Hoppa över\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Uppdatera\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Stäng\n    reopen: Reopen\n    ok: OK\n    light: Ljust\n    dark: Mörkt\n    system_setting: System setting\n    default: Standard\n    reset: Återställ\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignorera\n    submit: Skicka\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Sökresultat\n    keywords: Keywords\n    options: Alternativ\n    follow: Follow\n    following: Following\n    counts: \"{{count}} resultat\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Dela\n    copy: Kopiera länk\n    via: Dela inlägg via...\n    copied: Copied\n    facebook: Dela på Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Din e-postadress har uppdaterats.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Redigera\n    save: Spara\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} frågor\"\n    x_answers: \"{{ count }} svar\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bokmärken\n    reputation: Reputation\n    comments: Kommentarer\n    votes: Röster\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Redigera profil\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: Om mig\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Nästa\n    done: Klar\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Välj ett språk\n    db_type:\n      label: Database engine\n    db_username:\n      label: Användarnamn\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Lösenord\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Databasnamn\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Skapa config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Privat\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Namn\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Lösenord\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Varning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Användare\n    badges: Badges\n    flags: Flags\n    settings: Inställningar\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Användarvillkor\n    privacy: Privacy\n    seo: SEO\n    customize: Anpassa\n    themes: Teman\n    login: Logga in\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Välkommen till {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Använd {{ agentName }} för att skanna QR-koden och logga in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Grattis\n      content: You've earned a new badge.\n      close: Stäng\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Frågor:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Svar:\"\n      comments: \"Kommentarer:\"\n      votes: \"Röster:\"\n      users: \"Användare:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Privat\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Tidszon:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Databas:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Länkar\n      plugins: Plugins\n      github: GitHub\n      blog: Blogg\n      contact: Kontakt\n      forum: Forum\n      documents: Dokument\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Ja\"\n      \"no\": \"Nej\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Ändra användarroll till...\n      btn_cancel: Avbryt\n      btn_submit: Skicka\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Lösenord\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Avbryt\n      btn_submit: Skicka\n    edit_profile_modal:\n      title: Redigera profil\n      form:\n        fields:\n          display_name:\n            label: Visningsnamn\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Användarnamn\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Ogiltig e-postadress.\n      edit_success: Edited successfully\n      btn_cancel: Avbryt\n      btn_submit: Skicka\n    user_modal:\n      title: Lägg till ny användare\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separera \"namn, e-postadress, lösenord\" med kommatecken. En användare per rad.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Visningsnamn\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Lösenord\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Avbryt\n      btn_submit: Skicka\n    users:\n      title: Användare\n      name: Namn\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Roll\n      action: Action\n      change: Ändra\n      all: Alla\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: Användare\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Ändra status\n      change_role: Ändra roll\n      show_logs: Visa loggar\n      add_user: Lägg till användare\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Ändra\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Ändra\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Ange en giltig URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Tidszon\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Kryptering\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: Ingen\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Aktivera autentisering\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Ja\"\n        \"no\": \"Nej\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Användarvillkor\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Integritetspolicy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalänk\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Teman\n      themes:\n        label: Teman\n        text: Select an existing theme.\n      color_scheme:\n        label: Färgschema\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS och HTML\n      custom_css:\n        label: Anpassad CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Medlemskap\n        label: Tillåt nya registreringar\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: Alla\n        active: Aktiv\n        inactive: Inaktiv\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Namn\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Aktivera\n      settings: Inställningar\n    settings_users:\n      title: Användare\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Tillåt användare att ändra sitt visningsnamn\n      allow_update_username:\n        label: Tillåt användare att ändra sitt användarnamn\n      allow_update_avatar:\n        label: Tillåt användare att ändra sin profilbild\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Aktiv\n      activate: Aktivera\n      all: Alla\n      awards: Awards\n      deactivate: Inaktivera\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Grupp\n      inactive: Inaktiv\n      name: Namn\n      show_logs: Visa loggar\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Spara\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Redigera tagg\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Visa röster\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Användare\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: röster\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Dina ändringar kanske inte sparas.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/te_IN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: విజయవంతమైంది.\n    unknown:\n      other: తెలియని సమస్య.\n    request_format_error:\n      other: Request format is not valid.\n    unauthorized_error:\n      other: Unauthorized.\n    database_error:\n      other: Data server error.\n    forbidden_error:\n      other: Forbidden.\n    duplicate_request_error:\n      other: Duplicate submission.\n  action:\n    report:\n      other: Flag\n    edit:\n      other: Edit\n    delete:\n      other: Delete\n    close:\n      other: Close\n    reopen:\n      other: Reopen\n    forbidden_error:\n      other: Forbidden.\n    pin:\n      other: Pin\n    hide:\n      other: Unlist\n    unpin:\n      other: Unpin\n    show:\n      other: List\n    invite_someone_to_answer:\n      other: Edit\n    undelete:\n      other: Undelete\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: User\n      admin:\n        other: Admin\n      moderator:\n        other: Moderator\n    description:\n      user:\n        other: Default with no special access.\n      admin:\n        other: Have the full power to access the site.\n      moderator:\n        other: Has access to all posts except admin settings.\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: Write comment\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Password\n  pass:\n    other: Password\n  old_pass:\n    other: Current password\n  original_text:\n    other: This post\n  email_or_password_wrong_error:\n    other: Email and password do not match.\n  error:\n    common:\n      invalid_url:\n        other: Invalid URL.\n      status_invalid:\n        other: Invalid status.\n    password:\n      space_invalid:\n        other: Password cannot contain spaces.\n    admin:\n      cannot_update_their_password:\n        other: You cannot modify your password.\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: Email and password do not match.\n    answer:\n      not_found:\n        other: Answer do not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_update:\n        other: No permission to update.\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Comment are not allowed to edit.\n      not_found:\n        other: Comment not found.\n      cannot_edit_after_deadline:\n        other: The comment time has been too long to modify.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email already exists.\n      need_to_be_verified:\n        other: Email should be verified.\n      verify_url_expired:\n        other: Email verified URL has expired, please resend the email.\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: Language file not found.\n    object:\n      captcha_verification_failed:\n        other: Captcha wrong.\n      disallow_follow:\n        other: You are not allowed to follow.\n      disallow_vote:\n        other: You are not allowed to vote.\n      disallow_vote_your_self:\n        other: You can't vote for your own post.\n      not_found:\n        other: Object not found.\n      verification_failed:\n        other: Verification failed.\n      email_or_password_incorrect:\n        other: Email and password do not match.\n      old_password_verification_failed:\n        other: The old password verification failed\n      new_password_same_as_previous_setting:\n        other: The new password is the same as the previous one.\n      already_deleted:\n        other: This post has been deleted.\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: Question not found.\n      cannot_deleted:\n        other: No permission to delete.\n      cannot_close:\n        other: No permission to close.\n      cannot_update:\n        other: No permission to update.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: Report handle failed.\n      not_found:\n        other: Report not found.\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: Tag not found.\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: Please enter at least one required tag.\n      not_contain_synonym_tags:\n        other: Should not contain synonym tags.\n      cannot_update:\n        other: No permission to update.\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: You cannot set the synonym of the current tag as itself.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: Theme not found.\n    revision:\n      review_underway:\n        other: Can't edit currently, there is a version in the review queue.\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: Email and password do not match.\n      not_found:\n        other: User not found.\n      suspended:\n        other: User has been suspended.\n      username_invalid:\n        other: Username is invalid.\n      username_duplicate:\n        other: Username is already in use.\n      set_avatar:\n        other: Avatar set failed.\n      cannot_update_your_role:\n        other: You cannot modify your role.\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Read config failed\n    database:\n      connection_failed:\n        other: Database connection failed\n      create_table_failed:\n        other: Create table failed\n    install:\n      create_config_failed:\n        other: Can't create the config.yaml file.\n    upload:\n      unsupported_file_format:\n        other: Unsupported file format.\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: needs close\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: This question has been asked before and already has an answer.\n      guideline:\n        name:\n          other: a community-specific reason\n        desc:\n          other: This question doesn't meet a community guideline.\n      multiple:\n        name:\n          other: needs details or clarity\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: something else\n        desc:\n          other: This post requires another reason not listed above.\n    operation_type:\n      asked:\n        other: asked\n      answered:\n        other: answered\n      modified:\n        other: modified\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: updated question\n      answer_the_question:\n        other: answered question\n      update_answer:\n        other: updated answer\n      accept_answer:\n        other: accepted answer\n      comment_question:\n        other: commented question\n      comment_answer:\n        other: commented answer\n      reply_to_you:\n        other: replied to you\n      mention_you:\n        other: mentioned you\n      your_question_is_closed:\n        other: Your question has been closed\n      your_question_was_deleted:\n        other: Your question has been deleted\n      your_answer_was_deleted:\n        other: Your answer has been deleted\n      your_comment_was_deleted:\n        other: Your comment has been deleted\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: accept\n    accepted:\n      other: accepted\n    edit:\n      other: edit\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: Editor\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: First Link\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: Reader\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: Welcome\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: Thank You\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: How to Format\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: మునుపటి\n    next: Next\n  page_title:\n    question: ప్రశ్న\n    questions: ప్రశ్నలు\n    tag: ట్యాగ్\n    tags: టాగ్లు\n    tag_wiki: tag wiki\n    create_tag: ట్యాగ్‌ని సృష్టించండి\n    edit_tag: ట్యాగ్‌ని సవరించండి\n    ask_a_question: Create Question\n    edit_question: ప్రశ్నను సవరించండి\n    edit_answer: సమాధానాన్ని సవరించండి\n    search: Search\n    posts_containing: కలిగి ఉన్న పోస్ట్‌లు\n    settings: Settings\n    notifications: నోటిఫికేషన్‌లు\n    login: Log In\n    sign_up: Sign Up\n    account_recovery: Account Recovery\n    account_activation: Account Activation\n    confirm_email: Confirm Email\n    account_suspended: ఖాతా నిలిపివేయబడింది\n    admin: అడ్మిన్\n    change_email: Modify Email\n    install: Answer Installation\n    upgrade: Answer Upgrade\n    maintenance: Website Maintenance\n    users: Users\n    oauth_callback: Processing\n    http_404: HTTP Error 404\n    http_50X: HTTP Error 500\n    http_403: HTTP Error 403\n    logout: Log Out\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: నోటిఫికేషన్లు\n    inbox: ఇన్‌బాక్స్\n    achievement: విజయాలు\n    new_alerts: New alerts\n    all_read: Mark all as read\n    show_more: Show more\n    someone: Someone\n    inbox_type:\n      all: All\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: Your Account has been Suspended\n    until_time: \"Your account was suspended until {{ time }}.\"\n    forever: This user was suspended forever.\n    end: You don't meet a community guideline.\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: Blockquote\n    bold:\n      text: Strong\n    chart:\n      text: Chart\n      flow_chart: Flow chart\n      sequence_diagram: Sequence diagram\n      class_diagram: Class diagram\n      state_diagram: State diagram\n      entity_relationship_diagram: Entity relationship diagram\n      user_defined_diagram: User defined diagram\n      gantt_chart: Gantt chart\n      pie_chart: Pie chart\n    code:\n      text: Code Sample\n      add_code: Add code sample\n      form:\n        fields:\n          code:\n            label: Code\n            msg:\n              empty: Code cannot be empty.\n          language:\n            label: Language\n            placeholder: Automatic detection\n      btn_cancel: Cancel\n      btn_confirm: Add\n    formula:\n      text: Formula\n      options:\n        inline: Inline formula\n        block: Block formula\n    heading:\n      text: Heading\n      options:\n        h1: Heading 1\n        h2: Heading 2\n        h3: Heading 3\n        h4: Heading 4\n        h5: Heading 5\n        h6: Heading 6\n    help:\n      text: Help\n    hr:\n      text: Horizontal rule\n    image:\n      text: Image\n      add_image: Add image\n      tab_image: Upload image\n      form_image:\n        fields:\n          file:\n            label: Image file\n            btn: Select image\n            msg:\n              empty: File cannot be empty.\n              only_image: Only image files are allowed.\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: Description\n      tab_url: Image URL\n      form_url:\n        fields:\n          url:\n            label: Image URL\n            msg:\n              empty: Image URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n      uploading: Uploading\n    indent:\n      text: Indent\n    outdent:\n      text: Outdent\n    italic:\n      text: Emphasis\n    link:\n      text: Hyperlink\n      add_link: Add hyperlink\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL cannot be empty.\n          name:\n            label: Description\n      btn_cancel: Cancel\n      btn_confirm: Add\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: Table\n      heading: Heading\n      cell: Cell\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: I am closing this post as...\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n  report_modal:\n    flag_title: I am flagging to report this post as...\n    close_title: I am closing this post as...\n    review_question_title: Review question\n    review_answer_title: Review answer\n    review_comment_title: Review comment\n    btn_cancel: Cancel\n    btn_submit: Submit\n    remark:\n      empty: Cannot be empty.\n    msg:\n      empty: Please select a reason.\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: Create new tag\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: Display name cannot be empty.\n            range: Display name up to 35 characters.\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL slug cannot be empty.\n            range: URL slug up to 35 characters.\n            character: URL slug contains unallowed character set.\n        desc:\n          label: Description\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: Cancel\n    btn_submit: Submit\n    btn_post: Post new tag\n  tag_info:\n    created_at: Created\n    edited_at: Edited\n    history: History\n    synonyms:\n      title: Synonyms\n      text: The following tags will be remapped to\n      empty: No synonyms found.\n      btn_add: Add a synonym\n      btn_edit: Edit\n      btn_save: Save\n    synonyms_text: The following tags will be remapped to\n    delete:\n      title: Delete this tag\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: Are you sure you wish to delete?\n      close: Close\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Edit Tag\n    default_reason: Edit tag\n    default_first_reason: Add tag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: now\n    x_seconds_ago: \"{{count}}s ago\"\n    x_minutes_ago: \"{{count}}m ago\"\n    x_hours_ago: \"{{count}}h ago\"\n    hour: hour\n    day: day\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: Add comment\n    reply_to: Reply to\n    btn_reply: Reply\n    btn_edit: Edit\n    btn_delete: Delete\n    btn_flag: Flag\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n    show_more: \"{{count}} more comments\"\n    tip_question: >-\n      Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.\n    tip_answer: >-\n      Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: Edit Answer\n    default_reason: Edit answer\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: Revision\n        answer:\n          label: Answer\n          feedback:\n            characters: content must be at least 6 characters in length.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_save_edits: Save edits\n    btn_cancel: Cancel\n  tags:\n    title: Tags\n    sort_buttons:\n      popular: Popular\n      name: Name\n      newest: Newest\n    button_follow: Follow\n    button_following: Following\n    tag_label: questions\n    search_placeholder: Filter by tag name\n    no_desc: The tag has no description.\n    more: More\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Edit Question\n    default_reason: Edit question\n    default_first_reason: Create question\n    similar_questions: Similar questions\n    form:\n      fields:\n        revision:\n          label: Revision\n        title:\n          label: Title\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Title cannot be empty.\n            range: Title up to 150 characters\n        body:\n          label: Body\n          msg:\n            empty: Body cannot be empty.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Tags\n          msg:\n            empty: Tags cannot be empty.\n        answer:\n          label: Answer\n          msg:\n            empty: Answer cannot be empty.\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_post_question: Post your question\n    btn_save_edits: Save edits\n    answer_question: Answer your own question\n    post_question&answer: Post your question and answer\n  tag_selector:\n    add_btn: Add tag\n    create_btn: Create new tag\n    search_tag: Search tag\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: No tags matched\n    tag_required_text: Required tag (at least one)\n  header:\n    nav:\n      question: Questions\n      tag: Tags\n      user: Users\n      badges: Badges\n      profile: Profile\n      setting: Settings\n      logout: Log out\n      admin: Admin\n      review: Review\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: Search\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Change\n    loading: loading...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Type the text above\n    msg:\n      empty: Captcha cannot be empty.\n  inactive:\n    first: >-\n      You're almost done! We sent an activation mail to <bold>{{mail}}</bold>. Please follow the instructions in the mail to activate your account.\n    info: \"If it doesn't arrive, check your spam folder.\"\n    another: >-\n      We sent another activation email to you at <bold>{{mail}}</bold>. It might take a few minutes for it to arrive; be sure to check your spam folder.\n    btn_name: Resend activation email\n    change_btn_name: Change email\n    msg:\n      empty: Cannot be empty.\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: Log in to continue\n    info_sign: Don't have an account? <1>Sign up</1>\n    info_login: Already have an account? <1>Log in</1>\n    agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.\n    forgot_pass: Forgot password?\n    name:\n      label: Name\n      msg:\n        empty: Name cannot be empty.\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        different: The passwords entered on both sides are inconsistent\n  account_forgot:\n    page_title: Forgot Your Password\n    btn_name: Send me recovery email\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n  change_email:\n    btn_cancel: Cancel\n    btn_update: Update email address\n    send_success: >-\n      If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.\n    email:\n      label: New email\n      msg:\n        empty: Email cannot be empty.\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: Password Reset\n    btn_name: Reset my password\n    reset_success: >-\n      You successfully changed your password; you will be redirected to the log in page.\n    link_invalid: >-\n      Sorry, this password reset link is no longer valid. Perhaps your password is already reset?\n    to_login: Continue to log in page\n    password:\n      label: Password\n      msg:\n        empty: Password cannot be empty.\n        length: The length needs to be between 8 and 32\n        different: The passwords entered on both sides are inconsistent\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: Settings\n    goto_modify: Go to modify\n    nav:\n      profile: Profile\n      notification: Notifications\n      account: Account\n      interface: Interface\n    profile:\n      heading: Profile\n      btn_name: Save\n      display_name:\n        label: Display name\n        msg: Display name cannot be empty.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Username\n        caption: People can mention you as \"@username\".\n        msg: Username cannot be empty.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: Gravatar\n        gravatar_text: You can change image on\n        custom: Custom\n        custom_text: You can upload your image.\n        default: System\n        msg: Please upload an avatar\n      bio:\n        label: About me\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website incorrect format\n      location:\n        label: Location\n        placeholder: \"City, Country\"\n    notification:\n      heading: Email Notifications\n      turn_on: Turn on\n      inbox:\n        label: Inbox notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: Account\n      change_email_btn: Change email\n      change_pass_btn: Change password\n      change_email_info: >-\n        We've sent an email to that address. Please follow the confirmation instructions.\n      email:\n        label: Email\n      new_email:\n        label: New email\n        msg: New email cannot be empty.\n      pass:\n        label: Current password\n        msg: Password cannot be empty.\n      password_title: Password\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: The length needs to be between 8 and 32.\n          different: The two entered passwords do not match.\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: Confirm new password\n    interface:\n      heading: Interface\n      lang:\n        label: Interface language\n        text: User interface language. It will change when you refresh the page.\n    my_logins:\n      title: My logins\n      label: Log in or sign up on this site using these accounts.\n      modal_title: Remove login\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: update success\n    update_password: Password changed successfully.\n    flag_success: Thanks for flagging.\n    forbidden_operate_self: Forbidden to operate on yourself\n    review: Your revision will show after review.\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: answers\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Select people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: Asked\n    asked: asked\n    update: Modified\n    Edited: Edited\n    edit: edited\n    commented: commented\n    Views: Viewed\n    Follow: Follow\n    Following: Following\n    follow_tip: Follow this question to receive notifications\n    answered: answered\n    closed_in: Closed in\n    show_exist: Show existing question.\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: Answers\n      score: Score\n      newest: Newest\n      oldest: Oldest\n      btn_accept: Accept\n      btn_accepted: Accepted\n    write_answer:\n      title: Your Answer\n      edit_answer: Edit my existing answer\n      btn_name: Post your answer\n      add_another_answer: Add another answer\n      confirm_title: Continue to answer\n      continue: Continue\n      confirm_info: >-\n        <p>Are you sure you want to add another answer?</p><p>You could use the edit link to refine and improve your existing answer, instead.</p>\n      empty: Answer cannot be empty.\n      characters: content must be at least 6 characters in length.\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: Reopen this post\n      content: Are you sure you want to reopen?\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: Delete this post\n    question: >-\n      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?\n    answer_accepted: >-\n      <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?\n    other: Are you sure you wish to delete?\n    tip_answer_deleted: This answer has been deleted\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: Confirm\n    cancel: Cancel\n    edit: Edit\n    save: Save\n    delete: Delete\n    undelete: Undelete\n    list: List\n    unlist: Unlist\n    unlisted: Unlisted\n    login: Log in\n    signup: Sign up\n    logout: Log out\n    verify: Verify\n    create: Create\n    approve: Approve\n    reject: Reject\n    skip: Skip\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Search Results\n    keywords: Keywords\n    options: Options\n    follow: Follow\n    following: Following\n    counts: \"{{count}} Results\"\n    counts_loading: \"... Results\"\n    more: More\n    sort_btns:\n      relevance: Relevance\n      newest: Newest\n      active: Active\n      score: Score\n      more: More\n    tips:\n      title: Advanced Search Tips\n      tag: \"<1>[tag]</1> search with a tag\"\n      user: \"<1>user:username</1> search by author\"\n      answer: \"<1>answers:0</1> unanswered questions\"\n      score: \"<1>score:3</1> posts with a 3+ score\"\n      question: \"<1>is:question</1> search questions\"\n      is_answer: \"<1>is:answer</1> search answers\"\n    empty: We couldn't find anything. <br /> Try different or less specific keywords.\n  share:\n    name: Share\n    copy: Copy link\n    via: Share post via...\n    copied: Copied\n    facebook: Share to Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: Error...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Your new account is confirmed; you will be redirected to the home page.\n    link: Continue to homepage\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: Your email has been updated.\n    confirm_new_email_invalid: >-\n      Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?\n  unsubscribe:\n    page_title: Unsubscribe\n    success_title: Unsubscribe Successful\n    success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us.\n    link: Change settings\n  question:\n    following_tags: Following Tags\n    edit: Edit\n    save: Save\n    follow_tag_tip: Follow tags to curate your list of questions.\n    hot_questions: Hot Questions\n    all_questions: All Questions\n    x_questions: \"{{ count }} Questions\"\n    x_answers: \"{{ count }} answers\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Questions\n    answers: Answers\n    newest: Newest\n    active: Active\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: Score\n    unanswered: Unanswered\n    modified: modified\n    answered: answered\n    asked: asked\n    closed: closed\n    follow_a_tag: Follow a tag\n    more: More\n  personal:\n    overview: Overview\n    answers: Answers\n    answer: answer\n    questions: Questions\n    question: question\n    bookmarks: Bookmarks\n    reputation: Reputation\n    comments: Comments\n    votes: Votes\n    badges: Badges\n    newest: Newest\n    score: Score\n    edit_profile: Edit profile\n    visited_x_days: \"Visited {{ count }} days\"\n    viewed: Viewed\n    joined: Joined\n    comma: \",\"\n    last_login: Seen\n    about_me: About Me\n    about_me_empty: \"// Hello, World !\"\n    top_answers: Top Answers\n    top_questions: Top Questions\n    stats: Stats\n    list_empty: No posts found.<br />Perhaps you'd like to select a different tab?\n    content_empty: No posts found.\n    accepted: Accepted\n    answered: answered\n    asked: asked\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: Moderators\n    x_reputation: reputation\n    x_votes: votes received\n    x_answers: answers\n    x_questions: questions\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: Next\n    done: Done\n    config_yaml_error: Can't create the config.yaml file.\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: Username\n      placeholder: root\n      msg: Username cannot be empty.\n    db_password:\n      label: Password\n      placeholder: root\n      msg: Password cannot be empty.\n    db_host:\n      label: Database host\n      placeholder: \"db:3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: answer\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Create config.yaml\n      label: The config.yaml file created.\n      desc: >-\n        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.\n      info: After you've done that, click \"Next\" button.\n    site_information: Site Information\n    admin_account: Admin Account\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: Site URL\n      text: The address of your site.\n      msg:\n        empty: Site URL cannot be empty.\n        incorrect: Site URL incorrect format.\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: Email address of key contact responsible for this site.\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: Name\n      msg: Name cannot be empty.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Password\n      text: >-\n        You will need this password to log in. Please store it in a secure location.\n      msg: Password cannot be empty.\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: You will need this email to log in.\n      msg:\n        empty: Email cannot be empty.\n        incorrect: Email incorrect format.\n    ready_title: Your site is ready\n    ready_desc: >-\n      If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.\n    good_luck: \"Have fun, and good luck!\"\n    warn_title: Warning\n    warn_desc: >-\n      The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.\n    install_now: You may try <1>installing now</1>.\n    installed: Already installed\n    installed_desc: >-\n      You appear to have already installed. To reinstall please clear your old database tables first.\n    db_failed: Database connection failed\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: views\n    votes: votes\n    answers: answers\n    accepted: Accepted\n  page_error:\n    http_error: HTTP Error {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"We are under maintenance, we'll be back soon.\"\n  nav_menus:\n    dashboard: Dashboard\n    contents: Contents\n    questions: Questions\n    answers: Answers\n    users: Users\n    badges: Badges\n    flags: Flags\n    settings: Settings\n    general: General\n    interface: Interface\n    smtp: SMTP\n    branding: Branding\n    legal: Legal\n    write: Write\n    terms: Terms\n    tos: Terms of Service\n    privacy: Privacy\n    seo: SEO\n    customize: Customize\n    themes: Themes\n    login: Login\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: Admin\n    dashboard:\n      title: Dashboard\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"Questions:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"Answers:\"\n      comments: \"Comments:\"\n      votes: \"Votes:\"\n      users: \"Users:\"\n      flags: \"Flags:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"Version:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"Timezone:\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"Storage used:\"\n      uptime: \"Uptime:\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: Documents\n      feedback: Feedback\n      support: Support\n      review: Review\n      config: Config\n      update_to: Update to\n      latest: Latest\n      check_failed: Check failed\n      \"yes\": \"Yes\"\n      \"no\": \"No\"\n      not_allowed: Not allowed\n      allowed: Allowed\n      enabled: Enabled\n      disabled: Disabled\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: Flags\n      pending: Pending\n      completed: Completed\n      flagged: Flagged\n      flagged_type: Flagged {{ type }}\n      created: Created\n      action: Action\n      review: Review\n    user_role_modal:\n      title: Change user role to...\n      btn_cancel: Cancel\n      btn_submit: Submit\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: Users\n      name: Name\n      email: Email\n      reputation: Reputation\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Status\n      role: Role\n      action: Action\n      change: Change\n      all: All\n      staff: Staff\n      more: More\n      inactive: Inactive\n      suspended: Suspended\n      deleted: Deleted\n      normal: Normal\n      Moderator: Moderator\n      Admin: Admin\n      User: User\n      filter:\n        placeholder: \"Filter by name, user:id\"\n      set_new_password: Set new password\n      edit_profile: Edit profile\n      change_status: Change status\n      change_role: Change role\n      show_logs: Show logs\n      add_user: Add user\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Questions\n      unlisted: Unlisted\n      post: Post\n      votes: Votes\n      answers: Answers\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      pending: Pending\n      filter:\n        placeholder: \"Filter by title, question:id\"\n    answers:\n      page_title: Answers\n      post: Post\n      votes: Votes\n      created: Created\n      status: Status\n      action: Action\n      change: Change\n      filter:\n        placeholder: \"Filter by title, answer:id\"\n    general:\n      page_title: General\n      name:\n        label: Site name\n        msg: Site name cannot be empty.\n        text: \"The name of this site, as used in the title tag.\"\n      site_url:\n        label: Site URL\n        msg: Site url cannot be empty.\n        validate: Please enter a valid URL.\n        text: The address of your site.\n      short_desc:\n        label: Short site description\n        msg: Short site description cannot be empty.\n        text: \"Short description, as used in the title tag on homepage.\"\n      desc:\n        label: Site description\n        msg: Site description cannot be empty.\n        text: \"Describe this site in one sentence, as used in the meta description tag.\"\n      contact_email:\n        label: Contact email\n        msg: Contact email cannot be empty.\n        validate: Contact email is not valid.\n        text: Email address of key contact responsible for this site.\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: Interface\n      language:\n        label: Interface language\n        msg: Interface language cannot be empty.\n        text: User interface language. It will change when you refresh the page.\n      time_zone:\n        label: Timezone\n        msg: Timezone cannot be empty.\n        text: Choose a city in the same timezone as you.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: From email cannot be empty.\n        text: The email address which emails are sent from.\n      from_name:\n        label: From name\n        msg: From name cannot be empty.\n        text: The name which emails are sent from.\n      smtp_host:\n        label: SMTP host\n        msg: SMTP host cannot be empty.\n        text: Your mail server.\n      encryption:\n        label: Encryption\n        msg: Encryption cannot be empty.\n        text: For most servers SSL is the recommended option.\n        ssl: SSL\n        tls: TLS\n        none: None\n      smtp_port:\n        label: SMTP port\n        msg: SMTP port must be number 1 ~ 65535.\n        text: The port to your mail server.\n      smtp_username:\n        label: SMTP username\n        msg: SMTP username cannot be empty.\n      smtp_password:\n        label: SMTP password\n        msg: SMTP password cannot be empty.\n      test_email_recipient:\n        label: Test email recipients\n        text: Provide email address that will receive test sends.\n        msg: Test email recipients is invalid\n      smtp_authentication:\n        label: Enable authentication\n        title: SMTP authentication\n        msg: SMTP authentication cannot be empty.\n        \"yes\": \"Yes\"\n        \"no\": \"No\"\n    branding:\n      page_title: Branding\n      logo:\n        label: Logo\n        msg: Logo cannot be empty.\n        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.\n      mobile_logo:\n        label: Mobile logo\n        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.\n      square_icon:\n        label: Square icon\n        msg: Square icon cannot be empty.\n        text: Image used as the base for metadata icons. Should ideally be larger than 512x512.\n      favicon:\n        label: Favicon\n        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.\n    legal:\n      page_title: Legal\n      terms_of_service:\n        label: Terms of service\n        text: \"You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"Every new question must have at least one recommend tag.\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Permalink\n        text: Custom URL structures can improve the usability, and forward-compatibility of your links.\n      robots:\n        label: robots.txt\n        text: This will permanently override any related site settings.\n    themes:\n      page_title: Themes\n      themes:\n        label: Themes\n        text: Select an existing theme.\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Primary color\n        text: Modify the colors used by your themes\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS and HTML\n      custom_css:\n        label: Custom CSS\n        text: >\n\n      head:\n        label: Head\n        text: >\n\n      header:\n        label: Header\n        text: >\n\n      footer:\n        label: Footer\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: Login\n      membership:\n        title: Membership\n        label: Allow new registrations\n        text: Turn off to prevent anyone from creating a new account.\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: Private\n        label: Login required\n        text: Only logged in users can access this community.\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (optional)\n    empty: cannot be empty\n    invalid: is invalid\n    btn_submit: Save\n    not_found_props: \"Required property {{ key }} not found.\"\n    select: Select\n  page_review:\n    review: Review\n    proposed: proposed\n    question_edit: Question edit\n    answer_edit: Answer edit\n    tag_edit: Tag edit\n    edit_summary: Edit summary\n    edit_question: Edit question\n    edit_answer: Edit answer\n    edit_tag: Edit tag\n    empty: No review tasks left.\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: undeleted\n    deleted: deleted\n    downvote: downvote\n    upvote: upvote\n    accept: accept\n    cancelled: cancelled\n    commented: commented\n    rollback: rollback\n    edited: edited\n    answered: answered\n    asked: asked\n    closed: closed\n    reopened: reopened\n    created: created\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"History for\"\n    tag_title: \"Timeline for\"\n    show_votes: \"Show votes\"\n    n_or_a: N/A\n    title_for_question: \"Timeline for\"\n    title_for_answer: \"Timeline for answer to {{ title }} by {{ author }}\"\n    title_for_tag: \"Timeline for tag\"\n    datetime: Datetime\n    type: Type\n    by: By\n    comment: Comment\n    no_data: \"We couldn't find anything.\"\n  users:\n    title: Users\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: Our community staff\n    reputation: reputation\n    votes: votes\n  prompt:\n    leave_page: Are you sure you want to leave the page?\n    changes_not_save: Your changes may not be saved.\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/tr_TR.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Başarılı.\n    unknown:\n      other: Bilinmeyen hata.\n    request_format_error:\n      other: İstek formatı geçerli değil.\n    unauthorized_error:\n      other: Yetkisiz erişim.\n    database_error:\n      other: Veri tabanı sunucu hatası.\n    forbidden_error:\n      other: Erişim engellendi.\n    duplicate_request_error:\n      other: Yinelenen gönderim.\n  action:\n    report:\n      other: Bildir\n    edit:\n      other: Düzenle\n    delete:\n      other: Sil\n    close:\n      other: Kapat\n    reopen:\n      other: Yeniden Aç\n    forbidden_error:\n      other: Erişim engellendi.\n    pin:\n      other: Sabitle\n    hide:\n      other: Listeden Kaldır\n    unpin:\n      other: Sabitlemeyi Kaldır\n    show:\n      other: Listele\n    invite_someone_to_answer:\n      other: Cevaplamaya Davet Et\n    undelete:\n      other: Silmeyi Geri Al\n    merge:\n      other: Birleştir\n  role:\n    name:\n      user:\n        other: Kullanıcı\n      admin:\n        other: Yönetici\n      moderator:\n        other: Moderatör\n    description:\n      user:\n        other: Özel erişimi olmayan varsayılan kullanıcı.\n      admin:\n        other: Siteye tam erişim gücüne sahiptir.\n      moderator:\n        other: Yönetici ayarları dışında tüm gönderilere erişebilir.\n  privilege:\n    level_1:\n      description:\n        other: Seviye 1 (özel takım, grup için daha az itibar gerektirir)\n    level_2:\n      description:\n        other: Seviye 2 (başlangıç topluluğu için düşük itibar gerektirir)\n    level_3:\n      description:\n        other: Seviye 3 (gelişmiş topluluk için yüksek itibar gerektirir)\n    level_custom:\n      description:\n        other: Özel Seviye\n    rank_question_add_label:\n      other: Soru sor\n    rank_answer_add_label:\n      other: Cevap yaz\n    rank_comment_add_label:\n      other: Yorum yaz\n    rank_report_add_label:\n      other: Bildir\n    rank_comment_vote_up_label:\n      other: Yorumu yukarı oyla\n    rank_link_url_limit_label:\n      other: Bir seferde 2'den fazla bağlantı paylaş\n    rank_question_vote_up_label:\n      other: Soruyu yukarı oyla\n    rank_answer_vote_up_label:\n      other: Cevabı yukarı oyla\n    rank_question_vote_down_label:\n      other: Soruyu aşağı oyla\n    rank_answer_vote_down_label:\n      other: Cevabı aşağı oyla\n    rank_invite_someone_to_answer_label:\n      other: Birini cevaplamaya davet et\n    rank_tag_add_label:\n      other: Yeni etiket oluştur\n    rank_tag_edit_label:\n      other: Etiket açıklamasını düzenle (inceleme gerekli)\n    rank_question_edit_label:\n      other: Başkasının sorusunu düzenle (inceleme gerekli)\n    rank_answer_edit_label:\n      other: Başkasının cevabını düzenle (inceleme gerekli)\n    rank_question_edit_without_review_label:\n      other: Başkasının sorusunu inceleme olmadan düzenle\n    rank_answer_edit_without_review_label:\n      other: Başkasının cevabını inceleme olmadan düzenle\n    rank_question_audit_label:\n      other: Soru düzenlemelerini incele\n    rank_answer_audit_label:\n      other: Cevap düzenlemelerini incele\n    rank_tag_audit_label:\n      other: Etiket düzenlemelerini incele\n    rank_tag_edit_without_review_label:\n      other: Etiket açıklamasını inceleme olmadan düzenle\n    rank_tag_synonym_label:\n      other: Etiket eş anlamlılarını yönet\n  email:\n    other: E-posta\n  e_mail:\n    other: E-posta\n  password:\n    other: Parola\n  pass:\n    other: Parola\n  old_pass:\n    other: Mevcut parola\n  original_text:\n    other: Bu gönderi\n  email_or_password_wrong_error:\n    other: E-posta ve parola eşleşmiyor.\n  error:\n    common:\n      invalid_url:\n        other: Geçersiz URL.\n      status_invalid:\n        other: Geçersiz durum.\n    password:\n      space_invalid:\n        other: Parola boşluk içeremez.\n    admin:\n      cannot_update_their_password:\n        other: Parolanızı değiştiremezsiniz.\n      cannot_edit_their_profile:\n        other: Profilinizi değiştiremezsiniz.\n      cannot_modify_self_status:\n        other: Durumunuzu değiştiremezsiniz.\n      email_or_password_wrong:\n        other: E-posta ve parola eşleşmiyor.\n    answer:\n      not_found:\n        other: Cevap bulunamadı.\n      cannot_deleted:\n        other: Silme izni yok.\n      cannot_update:\n        other: Güncelleme izni yok.\n      question_closed_cannot_add:\n        other: Sorular kapatıldı ve cevap eklenemez.\n      content_cannot_empty:\n        other: Cevap içeriği boş olamaz.\n    comment:\n      edit_without_permission:\n        other: Yorumları düzenleme izniniz yok.\n      not_found:\n        other: Yorum bulunamadı.\n      cannot_edit_after_deadline:\n        other: Yorum süresi çok uzun olduğu için artık düzenlenemez.\n      content_cannot_empty:\n        other: Yorum içeriği boş olamaz.\n    email:\n      duplicate:\n        other: Bu e-posta adresi zaten kullanılmaktadır.\n      need_to_be_verified:\n        other: E-posta doğrulanmalıdır.\n      verify_url_expired:\n        other: E-posta doğrulama URL'sinin süresi dolmuş, lütfen e-postayı yeniden gönderin.\n      illegal_email_domain_error:\n        other: Bu e-posta alan adına izin verilmiyor. Lütfen başka bir e-posta kullanın.\n    lang:\n      not_found:\n        other: Dil dosyası bulunamadı.\n    object:\n      captcha_verification_failed:\n        other: Captcha yanlış.\n      disallow_follow:\n        other: Takip etme izniniz yok.\n      disallow_vote:\n        other: Oy verme izniniz yok.\n      disallow_vote_your_self:\n        other: Kendi gönderinize oy veremezsiniz.\n      not_found:\n        other: Nesne bulunamadı.\n      verification_failed:\n        other: Doğrulama başarısız.\n      email_or_password_incorrect:\n        other: E-posta ve parola eşleşmiyor.\n      old_password_verification_failed:\n        other: Eski parola doğrulaması başarısız oldu.\n      new_password_same_as_previous_setting:\n        other: Yeni parola öncekiyle aynı.\n      already_deleted:\n        other: Bu gönderi silinmiş.\n    meta:\n      object_not_found:\n        other: Meta nesnesi bulunamadı.\n    question:\n      already_deleted:\n        other: Bu gönderi silinmiş.\n      under_review:\n        other: Gönderiniz inceleme bekliyor. Onaylandıktan sonra görünür olacaktır.\n      not_found:\n        other: Soru bulunamadı.\n      cannot_deleted:\n        other: Silme izni yok.\n      cannot_close:\n        other: Kapatma izni yok.\n      cannot_update:\n        other: Güncelleme izni yok.\n      content_cannot_empty:\n        other: İçerik boş olamaz.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: İtibar seviyesi koşulu karşılamıyor.\n      vote_fail_to_meet_the_condition:\n        other: Geri bildiriminiz için teşekkürler. Oy kullanmak için en az {{.Rank}} itibara ihtiyacınız var.\n      no_enough_rank_to_operate:\n        other: Bu işlemi yapmak için en az {{.Rank}} itibara ihtiyacınız var.\n    report:\n      handle_failed:\n        other: Rapor işleme başarısız.\n      not_found:\n        other: Rapor bulunamadı.\n    tag:\n      already_exist:\n        other: Etiket zaten var.\n      not_found:\n        other: Etiket bulunamadı.\n      recommend_tag_not_found:\n        other: Önerilen etiket mevcut değil.\n      recommend_tag_enter:\n        other: Lütfen en az bir adet gerekli etiket giriniz.\n      not_contain_synonym_tags:\n        other: Eş anlamlı etiketler içermemelidir.\n      cannot_update:\n        other: Güncelleme izni yok.\n      is_used_cannot_delete:\n        other: Kullanımda olan bir etiketi silemezsiniz.\n      cannot_set_synonym_as_itself:\n        other: Bir etiketin eş anlamlısını kendisi olarak ayarlayamazsınız.\n      minimum_count:\n        other: Yeterli etiket girilmedi.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Gönderen adı bir e-posta adresi olamaz.\n    theme:\n      not_found:\n        other: Tema bulunamadı.\n    revision:\n      review_underway:\n        other: Şu anda düzenlenemez, inceleme kuyruğunda bir sürüm var.\n      no_permission:\n        other: Düzenleme izniniz yok.\n    user:\n      external_login_missing_user_id:\n        other: Üçüncü taraf platform benzersiz bir Kullanıcı ID'si sağlamıyor, bu nedenle giriş yapamazsınız. Lütfen site yöneticisiyle iletişime geçin.\n      external_login_unbinding_forbidden:\n        other: Bu girişi kaldırmadan önce lütfen hesabınız için bir giriş parolası ayarlayın.\n      email_or_password_wrong:\n        other:\n          other: E-posta ve parola eşleşmiyor.\n      not_found:\n        other: Kullanıcı bulunamadı.\n      suspended:\n        other: Kullanıcı askıya alındı.\n      username_invalid:\n        other: Kullanıcı adı geçersiz.\n      username_duplicate:\n        other: Kullanıcı adı zaten kullanımda.\n      set_avatar:\n        other: Avatar ayarlama başarısız.\n      cannot_update_your_role:\n        other: Kendi rolünüzü değiştiremezsiniz.\n      not_allowed_registration:\n        other: Şu anda site kayıt için açık değil.\n      not_allowed_login_via_password:\n        other: Şu anda site parola ile giriş yapmaya izin vermiyor.\n      access_denied:\n        other: Erişim reddedildi.\n      page_access_denied:\n        other: Bu sayfaya erişim izniniz yok.\n      add_bulk_users_format_error:\n        other: \"{{.Line}} satırındaki '{{.Content}}' içeriğinde {{.Field}} biçimi hatası. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Bir kerede eklediğiniz kullanıcı sayısı 1-{{.MaxAmount}} aralığında olmalıdır.\"\n      status_suspended_forever:\n        other: \"<strong>Bu kullanıcı kalıcı olarak uzaklaştırıldı.</strong> Bu kullanıcı topluluk yönergelerine uymuyor.\"\n      status_suspended_until:\n        other: \"<strong>Bu kullanıcı {{.SuspendedUntil}} tarihine kadar askıya alındı.</strong> Bu kullanıcı topluluk yönergelerine uymuyor.\"\n      status_deleted:\n        other: \"Bu kullanıcı silindi.\"\n      status_inactive:\n        other: \"Bu kullanıcı aktif değil.\"\n    config:\n      read_config_failed:\n        other: Yapılandırma okunamadı.\n    database:\n      connection_failed:\n        other: Veritabanı bağlantısı başarısız.\n      create_table_failed:\n        other: Tablo oluşturma başarısız.\n    install:\n      create_config_failed:\n        other: config.yaml dosyası oluşturulamıyor.\n    upload:\n      unsupported_file_format:\n        other: Desteklenmeyen dosya formatı.\n    site_info:\n      config_not_found:\n        other: Site yapılandırması bulunamadı.\n    badge:\n      object_not_found:\n        other: Rozet nesnesi bulunamadı.\n  reason:\n    spam:\n      name:\n        other: spam\n      desc:\n        other: Bu gönderi bir reklam veya vandalizm. Mevcut konuyla ilgili veya yararlı değil.\n    rude_or_abusive:\n      name:\n        other: kaba veya taciz edici\n      desc:\n        other: \"Makul bir kişi bu içeriği saygılı bir iletişim için uygunsuz bulurdu.\"\n    a_duplicate:\n      name:\n        other: kopya\n      desc:\n        other: Bu soru daha önce sorulmuş ve cevaplandırılmış.\n      placeholder:\n        other: Mevcut soru bağlantısını girin\n    not_a_answer:\n      name:\n        other: cevap değil\n      desc:\n        other: \"Bu bir cevap olarak gönderilmiş, ancak soruyu cevaplamaya çalışmıyor. Bir düzenleme, yorum, başka bir soru olabilir veya tamamen silinmesi gerekebilir.\"\n    no_longer_needed:\n      name:\n        other: artık gerekli değil\n      desc:\n        other: Bu yorum güncelliğini yitirmiş, sohbet niteliğinde veya bu gönderiyle ilgili değil.\n    something:\n      name:\n        other: başka bir şey\n      desc:\n        other: Bu gönderi, yukarıda listelenmeyen başka bir nedenden dolayı personel ilgisi gerektiriyor.\n      placeholder:\n        other: Endişelerinizin ne olduğunu spesifik olarak belirtin\n    community_specific:\n      name:\n        other: topluluk kurallarına aykırı\n      desc:\n        other: Bu soru bir topluluk kılavuzuna uymuyor.\n    not_clarity:\n      name:\n        other: detay veya açıklık gerekiyor\n      desc:\n        other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı.\n    looks_ok:\n      name:\n        other: iyi görünüyor\n      desc:\n        other: Bu gönderi olduğu gibi iyi ve düşük kaliteli değil.\n    needs_edit:\n      name:\n        other: düzenleme gerektiriyor ve ben yaptım\n      desc:\n        other: Bu gönderideki sorunları kendiniz düzeltin ve iyileştirin.\n    needs_close:\n      name:\n        other: kapatılması gerekiyor\n      desc:\n        other: Kapatılmış bir soru cevaplanamaz, ancak düzenlenebilir, oylanabilir ve yorum yapılabilir.\n    needs_delete:\n      name:\n        other: silinmesi gerekiyor\n      desc:\n        other: Bu gönderi silinecek.\n  question:\n    close:\n      duplicate:\n        name:\n          other: kopya\n        desc:\n          other: Bu soru daha önce sorulmuş ve cevaplandırılmış.\n      guideline:\n        name:\n          other: topluluk kurallarına aykırı\n        desc:\n          other: Bu soru bir topluluk kılavuzuna uymuyor.\n      multiple:\n        name:\n          other: detay veya açıklık gerekiyor\n        desc:\n          other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı.\n      other:\n        name:\n          other: başka bir şey\n        desc:\n          other: Bu gönderi yukarıda listelenmeyen başka bir neden gerektiriyor.\n    operation_type:\n      asked:\n        other: soruldu\n      answered:\n        other: cevaplandı\n      modified:\n        other: değiştirildi\n    deleted_title:\n      other: Silinmiş soru\n    questions_title:\n      other: Sorular\n  tag:\n    tags_title:\n      other: Etiketler\n    no_description:\n      other: Bu etiketin açıklaması yok.\n  notification:\n    action:\n      update_question:\n        other: soruyu güncelledi\n      answer_the_question:\n        other: soruyu cevapladı\n      update_answer:\n        other: cevabı güncelledi\n      accept_answer:\n        other: cevabı kabul etti\n      comment_question:\n        other: soruya yorum yaptı\n      comment_answer:\n        other: cevaba yorum yaptı\n      reply_to_you:\n        other: size yanıt verdi\n      mention_you:\n        other: sizden bahsetti\n      your_question_is_closed:\n        other: Sorunuz kapatıldı\n      your_question_was_deleted:\n        other: Sorunuz silindi\n      your_answer_was_deleted:\n        other: Cevabınız silindi\n      your_comment_was_deleted:\n        other: Yorumunuz silindi\n      up_voted_question:\n        other: soruyu yukarı oyladı\n      down_voted_question:\n        other: soruyu aşağı oyladı\n      up_voted_answer:\n        other: cevabı yukarı oyladı\n      down_voted_answer:\n        other: cevabı aşağı oyladı\n      up_voted_comment:\n        other: yorumu yukarı oyladı\n      invited_you_to_answer:\n        other: sizi cevaplamaya davet etti\n      earned_badge:\n        other: Rozet kazandınız \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Yeni e-posta adresinizi onaylayın\"\n      body:\n        other: \"{{.SiteName}} için aşağıdaki bağlantıya tıklayarak yeni e-posta adresinizi onaylayın:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nEğer bu değişikliği siz talep etmediyseniz, lütfen bu e-postayı dikkate almayın.<br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} sorunuzu cevapladı\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>{{.SiteName}} üzerinde görüntüle</a><br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abonelikten çık</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} sizi cevaplamaya davet etti\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Cevabı biliyor olabileceğinizi düşünüyorum.</blockquote><br>\\n<a href='{{.InviteUrl}}'>{{.SiteName}} üzerinde görüntüle</a><br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abonelikten çık</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} gönderinize yorum yaptı\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>{{.SiteName}} üzerinde görüntüle</a><br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abonelikten çık</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Yeni soru: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Abonelikten çık</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Parola sıfırlama\"\n      body:\n        other: \"Birisi {{.SiteName}} üzerindeki parolanızı sıfırlamak istedi.<br><br>\\n\\nEğer bu siz değilseniz, bu e-postayı güvenle görmezden gelebilirsiniz.<br><br>\\n\\nYeni bir parola seçmek için aşağıdaki bağlantıya tıklayın:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Yeni hesabınızı onaylayın\"\n      body:\n        other: \"{{.SiteName}} sitesine hoş geldiniz!<br><br>\\n\\nYeni hesabınızı onaylamak ve etkinleştirmek için aşağıdaki bağlantıya tıklayın:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nYukarıdaki bağlantı tıklanabilir değilse, web tarayıcınızın adres çubuğuna kopyalayıp yapıştırmayı deneyin.\\n<br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test E-postası\"\n      body:\n        other: \"Bu bir test e-postasıdır.\\n<br><br>\\n\\n--<br>\\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.\"\n  action_activity_type:\n    upvote:\n      other: yukarı oyla\n    upvoted:\n      other: yukarı oyladı\n    downvote:\n      other: aşağı oyla\n    downvoted:\n      other: aşağı oyladı\n    accept:\n      other: kabul et\n    accepted:\n      other: kabul edildi\n    edit:\n      other: düzenle\n  review:\n    queued_post:\n      other: Sıradaki gönderi\n    flagged_post:\n      other: Bildirilen gönderi\n    suggested_post_edit:\n      other: Önerilen düzenlemeler\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} ve {{ .Count }} kişi daha...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Otobiyografi Yazarı\n        desc:\n          other: <a href=\"{{ .ProfileURL }}\" target=\"_blank\">Profil</a> bilgilerini doldurdu.\n      certified:\n        name:\n          other: Sertifikalı\n        desc:\n          other: Yeni kullanıcı eğitimimizi tamamladı.\n      editor:\n        name:\n          other: Editör\n        desc:\n          other: İlk gönderi düzenlemesi.\n      first_flag:\n        name:\n          other: İlk Bildirim\n        desc:\n          other: İlk kez bir gönderiyi bildirdi.\n      first_upvote:\n        name:\n          other: İlk Yukarı Oylama\n        desc:\n          other: İlk kez bir gönderiyi yukarı oyladı.\n      first_link:\n        name:\n          other: İlk Bağlantı\n        desc:\n          other: İlk kez başka bir gönderiye bağlantı ekledi.\n      first_reaction:\n        name:\n          other: İlk Tepki\n        desc:\n          other: İlk kez bir gönderiye tepki verdi.\n      first_share:\n        name:\n          other: İlk Paylaşım\n        desc:\n          other: İlk kez bir gönderi paylaştı.\n      scholar:\n        name:\n          other: Bilim İnsanı\n        desc:\n          other: Bir soru sordu ve bir cevabı kabul etti.\n      commentator:\n        name:\n          other: Yorumcu\n        desc:\n          other: 5 yorum bıraktı.\n      new_user_of_the_month:\n        name:\n          other: Ayın Yeni Kullanıcısı\n        desc:\n          other: İlk aylarında üstün katkılarda bulundu.\n      read_guidelines:\n        name:\n          other: Kuralları Okuyan\n        desc:\n          other: '[Topluluk kurallarını] oku.'\n      reader:\n        name:\n          other: Okuyucu\n        desc:\n          other: 10'dan fazla cevap içeren bir konudaki tüm cevapları okudu.\n      welcome:\n        name:\n          other: Hoş Geldin\n        desc:\n          other: Bir yukarı oy aldı.\n      nice_share:\n        name:\n          other: Güzel Paylaşım\n        desc:\n          other: 25 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı.\n      good_share:\n        name:\n          other: İyi Paylaşım\n        desc:\n          other: 300 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı.\n      great_share:\n        name:\n          other: Harika Paylaşım\n        desc:\n          other: 1000 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı.\n      out_of_love:\n        name:\n          other: Sevgiden\n        desc:\n          other: Bir günde 50 yukarı oy kullandı.\n      higher_love:\n        name:\n          other: Yüksek Sevgi\n        desc:\n          other: Bir günde 50 yukarı oyu 5 kez kullandı.\n      crazy_in_love:\n        name:\n          other: Çılgınca Aşık\n        desc:\n          other: Bir günde 50 yukarı oyu 20 kez kullandı.\n      promoter:\n        name:\n          other: Destekçi\n        desc:\n          other: Bir kullanıcıyı davet etti.\n      campaigner:\n        name:\n          other: Kampanyacı\n        desc:\n          other: 3 temel kullanıcıyı davet etti.\n      champion:\n        name:\n          other: Şampiyon\n        desc:\n          other: 5 üye davet etti.\n      thank_you:\n        name:\n          other: Teşekkürler\n        desc:\n          other: 20 yukarı oy aldı ve 10 yukarı oy verdi.\n      gives_back:\n        name:\n          other: Karşılık Veren\n        desc:\n          other: 100 yukarı oy aldı ve 100 yukarı oy verdi.\n      empathetic:\n        name:\n          other: Empatik\n        desc:\n          other: 500 yukarı oy aldı ve 1000 yukarı oy verdi.\n      enthusiast:\n        name:\n          other: Hevesli\n        desc:\n          other: 10 gün üst üste ziyaret etti.\n      aficionado:\n        name:\n          other: Meraklı\n        desc:\n          other: 100 gün üst üste ziyaret etti.\n      devotee:\n        name:\n          other: Hayran\n        desc:\n          other: 365 gün üst üste ziyaret etti.\n      anniversary:\n        name:\n          other: Yıldönümü\n        desc:\n          other: Bir yıl boyunca aktif üye, en az bir gönderi paylaştı.\n      appreciated:\n        name:\n          other: Takdir Edilen\n        desc:\n          other: 20 gönderisinde 1 yukarı oy aldı.\n      respected:\n        name:\n          other: Saygın\n        desc:\n          other: 100 gönderisinde 2 yukarı oy aldı.\n      admired:\n        name:\n          other: Hayranlık Uyandıran\n        desc:\n          other: 300 gönderisinde 5 yukarı oy aldı.\n      solved:\n        name:\n          other: Çözüldü\n        desc:\n          other: Bir cevabı kabul edildi.\n      guidance_counsellor:\n        name:\n          other: Rehber Danışman\n        desc:\n          other: 10 cevabı kabul edildi.\n      know_it_all:\n        name:\n          other: Her Şeyi Bilen\n        desc:\n          other: 50 cevabı kabul edildi.\n      solution_institution:\n        name:\n          other: Çözüm Kurumu\n        desc:\n          other: 150 cevabı kabul edildi.\n      nice_answer:\n        name:\n          other: Güzel Cevap\n        desc:\n          other: 10 veya daha fazla cevap puanı.\n      good_answer:\n        name:\n          other: İyi Cevap\n        desc:\n          other: 25 veya daha fazla cevap puanı.\n      great_answer:\n        name:\n          other: Harika Cevap\n        desc:\n          other: 50 veya daha fazla cevap puanı.\n      nice_question:\n        name:\n          other: Güzel Soru\n        desc:\n          other: 10 veya daha fazla soru puanı.\n      good_question:\n        name:\n          other: İyi Soru\n        desc:\n          other: 25 veya daha fazla soru puanı.\n      great_question:\n        name:\n          other: Harika Soru\n        desc:\n          other: 50 veya daha fazla soru puanı.\n      popular_question:\n        name:\n          other: Popüler Soru\n        desc:\n          other: 500 görüntülenme alan soru.\n      notable_question:\n        name:\n          other: Dikkat Çeken Soru\n        desc:\n          other: 1.000 görüntülenme alan soru.\n      famous_question:\n        name:\n          other: Ünlü Soru\n        desc:\n          other: 5.000 görüntülenme alan soru.\n      popular_link:\n        name:\n          other: Popüler Bağlantı\n        desc:\n          other: 50 tıklama alan harici bir bağlantı paylaştı.\n      hot_link:\n        name:\n          other: Sıcak Bağlantı\n        desc:\n          other: 300 tıklama alan harici bir bağlantı paylaştı.\n      famous_link:\n        name:\n          other: Ünlü Bağlantı\n        desc:\n          other: 100 tıklama alan harici bir bağlantı paylaştı.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Başlangıç\n      community:\n        name:\n          other: Topluluk\n      posting:\n        name:\n          other: Gönderi Yazmak\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Nasıl Biçimlendirilir\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">bir gönderiden bahsetmek için: <code>#post_id</code></p></li> <li><p class=\"mb-2\">bağlantı oluşturmak için</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[Başlık](https://url.com)</code></pre></li><li><p class=\"mb-2\">paragraflar arasında boşluk bırakın</p></li><li><p class=\"mb-2\"><em>_italik_</em> veya **<strong>kalın</strong>**</p></li><li><p class=\"mb-2\">kodu 4 boşlukla girintileyin</p></li><li><p class=\"mb-2\">satırın başına <code>&gt;</code> koyarak alıntı yapın</p></li><li><p class=\"mb-2\">ters tırnak işaretleriyle kaçış yapın <code>`_böyle_`</code></p></li><li><p class=\"mb-2\">ters tırnak işaretleriyle <code>`</code> kod blokları oluşturun</p><pre class=\"mb-0\"><code>```<br/>kod buraya<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Önceki\n    next: Sonraki\n  page_title:\n    question: Soru\n    questions: Sorular\n    tag: Etiket\n    tags: Etiketler\n    tag_wiki: etiket wikisi\n    create_tag: Etiket Oluştur\n    edit_tag: Etiketi Düzenle\n    ask_a_question: Soru Oluştur\n    edit_question: Soruyu Düzenle\n    edit_answer: Cevabı Düzenle\n    search: Ara\n    posts_containing: İçeren gönderiler\n    settings: Ayarlar\n    notifications: Bildirimler\n    login: Giriş Yap\n    sign_up: Kayıt Ol\n    account_recovery: Hesap Kurtarma\n    account_activation: Hesap Aktivasyonu\n    confirm_email: E-posta Onayı\n    account_suspended: Hesap Askıya Alındı\n    admin: Yönetici\n    change_email: E-posta Değiştir\n    install: Answer Kurulumu\n    upgrade: Answer Yükseltme\n    maintenance: Website Bakımı\n    users: Kullanıcılar\n    oauth_callback: İşleniyor\n    http_404: HTTP Hatası 404\n    http_50X: HTTP Hatası 500\n    http_403: HTTP Hatası 403\n    logout: Çıkış Yap\n    posts: Gönderiler\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Bildirimler\n    inbox: Gelen Kutusu\n    achievement: Başarılar\n    new_alerts: Yeni uyarılar\n    all_read: Tümünü okundu olarak işaretle\n    show_more: Daha fazla göster\n    someone: Birisi\n    inbox_type:\n      all: Tümü\n      posts: Gönderiler\n      invites: Davetler\n      votes: Oylar\n    answer: Cevap\n    question: Soru\n    badge_award: Rozet\n  suspended:\n    title: Hesabınız Askıya Alındı\n    until_time: \"Hesabınız {{ time }} tarihine kadar askıya alındı.\"\n    forever: Bu kullanıcı süresiz olarak askıya alındı.\n    end: Topluluk kurallarını karşılamıyorsunuz.\n    contact_us: Bize ulaşın\n  editor:\n    blockquote:\n      text: Alıntı\n    bold:\n      text: Kalın\n    chart:\n      text: Grafik\n      flow_chart: Akış şeması\n      sequence_diagram: Sıralama diyagramı\n      class_diagram: Sınıf diyagramı\n      state_diagram: Durum diyagramı\n      entity_relationship_diagram: Varlık ilişki diyagramı\n      user_defined_diagram: Kullanıcı tanımlı diyagram\n      gantt_chart: Gantt şeması\n      pie_chart: Pasta grafiği\n    code:\n      text: Kod Örneği\n      add_code: Kod örneği ekle\n      form:\n        fields:\n          code:\n            label: Kod\n            msg:\n              empty: Kod boş olamaz.\n          language:\n            label: Dil\n            placeholder: Otomatik algılama\n      btn_cancel: İptal\n      btn_confirm: Ekle\n    formula:\n      text: Formül\n      options:\n        inline: Satır içi formül\n        block: Blok formül\n    heading:\n      text: Başlık\n      options:\n        h1: Başlık 1\n        h2: Başlık 2\n        h3: Başlık 3\n        h4: Başlık 4\n        h5: Başlık 5\n        h6: Başlık 6\n    help:\n      text: Yardım\n    hr:\n      text: Yatay çizgi\n    image:\n      text: Resim\n      add_image: Resim ekle\n      tab_image: Resim Yükle\n      form_image:\n        fields:\n          file:\n            label: Resim dosyası\n            btn: Resim seç\n            msg:\n              empty: Dosya boş olamaz.\n              only_image: Sadece resim dosyalarına izin verilir.\n              max_size: Dosya boyutu {{size}} MB'ı geçemez.\n          desc:\n            label: Açıklama\n      tab_url: Resim URL'si\n      form_url:\n        fields:\n          url:\n            label: Resim URL'si\n            msg:\n              empty: Resim URL'si boş olamaz.\n          name:\n            label: Açıklama\n      btn_cancel: İptal\n      btn_confirm: Ekle\n      uploading: Yükleniyor\n    indent:\n      text: Girinti\n    outdent:\n      text: Girintiyi azalt\n    italic:\n      text: İtalik\n    link:\n      text: Bağlantı\n      add_link: Bağlantı ekle\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL boş olamaz.\n          name:\n            label: Açıklama\n      btn_cancel: İptal\n      btn_confirm: Ekle\n    ordered_list:\n      text: Numaralı liste\n    unordered_list:\n      text: Madde işaretli liste\n    table:\n      text: Tablo\n      heading: Başlık\n      cell: Hücre\n    file:\n      text: Dosya ekle\n      not_supported: \"Bu dosya türü desteklenmiyor. {{file_type}} ile tekrar deneyin.\"\n      max_size: \"Eklenen dosyaların boyutu {{size}} MB'ı geçemez.\"\n  close_modal:\n    title: Bu gönderiyi kapatıyorum çünkü...\n    btn_cancel: İptal\n    btn_submit: Gönder\n    remark:\n      empty: Boş olamaz.\n    msg:\n      empty: Lütfen bir neden seçin.\n  report_modal:\n    flag_title: Bu gönderiyi şu nedenle bildiriyorum...\n    close_title: Bu gönderiyi şu nedenle kapatıyorum...\n    review_question_title: Soruyu incele\n    review_answer_title: Cevabı incele\n    review_comment_title: Yorumu incele\n    btn_cancel: İptal\n    btn_submit: Gönder\n    remark:\n      empty: Boş olamaz.\n    msg:\n      empty: Lütfen bir neden seçin.\n      not_a_url: URL formatı yanlış.\n      url_not_match: URL kaynağı mevcut web sitesiyle eşleşmiyor.\n  tag_modal:\n    title: Yeni etiket oluştur\n    form:\n      fields:\n        display_name:\n          label: Görünen ad\n          msg:\n            empty: Görünen ad boş olamaz.\n            range: Görünen ad en fazla 35 karakter olabilir.\n        slug_name:\n          label: URL kısaltması\n          desc: URL kısaltması en fazla 35 karakter olabilir.\n          msg:\n            empty: URL kısaltması boş olamaz.\n            range: URL kısaltması en fazla 35 karakter olabilir.\n            character: URL kısaltması izin verilmeyen karakter içeriyor.\n        desc:\n          label: Açıklama\n        revision:\n          label: Revizyon\n        edit_summary:\n          label: Düzenleme özeti\n          placeholder: >-\n            Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi)\n    btn_cancel: İptal\n    btn_submit: Gönder\n    btn_post: Yeni etiket gönder\n  tag_info:\n    created_at: Oluşturuldu\n    edited_at: Düzenlendi\n    history: Geçmiş\n    synonyms:\n      title: Eş Anlamlılar\n      text: Aşağıdaki etiketler şuna yeniden eşlenecek\n      empty: Eş anlamlı bulunamadı.\n      btn_add: Eş anlamlı ekle\n      btn_edit: Düzenle\n      btn_save: Kaydet\n    synonyms_text: Aşağıdaki etiketler şuna yeniden eşlenecek\n    delete:\n      title: Bu etiketi sil\n      tip_with_posts: >-\n        <p><strong>Gönderileri olan etiketin silinmesine</strong> izin vermiyoruz.</p> <p>Lütfen önce bu etiketi gönderilerden kaldırın.</p>\n      tip_with_synonyms: >-\n        <p><strong>Eş anlamlıları olan etiketin silinmesine</strong> izin vermiyoruz.</p> <p>Lütfen önce bu etiketin eş anlamlılarını kaldırın.</p>\n      tip: Silmek istediğinizden emin misiniz?\n      close: Kapat\n    merge:\n      title: Etiket birleştir\n      source_tag_title: Kaynak etiket\n      source_tag_description: Kaynak etiket ve ilişkili verileri hedef etikete yeniden eşlenecek.\n      target_tag_title: Hedef etiket\n      target_tag_description: Birleştirmeden sonra bu iki etiket arasında bir eş anlamlı ilişkisi oluşturulacak.\n      no_results: Eşleşen etiket bulunamadı\n      btn_submit: Gönder\n      btn_close: Kapat\n  edit_tag:\n    title: Etiketi Düzenle\n    default_reason: Etiketi düzenle\n    default_first_reason: Etiket ekle\n    btn_save_edits: Düzenlemeleri kaydet\n    btn_cancel: İptal\n  dates:\n    long_date: D MMM\n    long_date_with_year: \"D MMM, YYYY\"\n    long_date_with_time: \"D MMM, YYYY [saat] HH:mm\"\n    now: şimdi\n    x_seconds_ago: \"{{count}} saniye önce\"\n    x_minutes_ago: \"{{count}} dakika önce\"\n    x_hours_ago: \"{{count}} saat önce\"\n    hour: saat\n    day: gün\n    hours: saatler\n    days: günler\n    month: ay\n    months: aylar\n    year: yıl\n  reaction:\n    heart: kalp\n    smile: gülümseme\n    frown: üzgün\n    btn_label: tepki ekle veya kaldır\n    undo_emoji: '{{emoji}} tepkisini geri al'\n    react_emoji: '{{emoji}} ile tepki ver'\n    unreact_emoji: '{{emoji}} tepkisini kaldır'\n  comment:\n    btn_add_comment: Yorum ekle\n    reply_to: Yanıtla\n    btn_reply: Yanıtla\n    btn_edit: Düzenle\n    btn_delete: Sil\n    btn_flag: Bildir\n    btn_save_edits: Düzenlemeleri kaydet\n    btn_cancel: İptal\n    show_more: \"{{count}} daha fazla yorum\"\n    tip_question: >-\n      Daha fazla bilgi istemek veya iyileştirmeler önermek için yorumları kullanın. Yorumlarda soruları cevaplamaktan kaçının.\n    tip_answer: >-\n      Diğer kullanıcılara yanıt vermek veya onları değişikliklerden haberdar etmek için yorumları kullanın. Yeni bilgi ekliyorsanız, yorum yapmak yerine gönderinizi düzenleyin.\n    tip_vote: Gönderiye faydalı bir şey ekliyor\n  edit_answer:\n    title: Cevabı Düzenle\n    default_reason: Cevabı düzenle\n    default_first_reason: Cevap ekle\n    form:\n      fields:\n        revision:\n          label: Revizyon\n        answer:\n          label: Cevap\n          feedback:\n            characters: içerik en az 6 karakter uzunluğunda olmalıdır.\n        edit_summary:\n          label: Düzenleme özeti\n          placeholder: >-\n            Yaptığınız değişiklikleri kısaca açıklayın (düzeltilmiş yazım geliştirilmiş biçimlendirme)\n    btn_save_edits: Düzenlemeleri kaydet\n    btn_cancel: İptal\n  tags:\n    title: Etiketler\n    sort_buttons:\n      popular: Popüler\n      name: İsim\n      newest: En Yeni\n    button_follow: Takip Et\n    button_following: Takip Ediliyor\n    tag_label: sorular\n    search_placeholder: Etiket adına göre filtrele\n    no_desc: Bu etiketin açıklaması yok.\n    more: Daha Fazla\n    wiki: Wiki\n  ask:\n    title: Soru Oluştur\n    edit_title: Soruyu Düzenle\n    default_reason: Soruyu düzenle\n    default_first_reason: Soru oluştur\n    similar_questions: Benzer sorular\n    form:\n      fields:\n        revision:\n          label: Revizyon\n        title:\n          label: Başlık\n          placeholder: Konu nedir? Ayrıntılı yaz.\n          msg:\n            empty: Başlık boş olamaz.\n            range: Başlık en fazla 150 karakter olabilir\n        body:\n          label: İçerik\n          msg:\n            empty: İçerik boş olamaz.\n          hint:\n            optional_body: Sorunun neyle ilgili olduğunu açıklayın.\n            minimum_characters: \"Sorunun neyle ilgili olduğunu açıklayın, en az {{min_content_length}} karakter gereklidir.\"\n        tags:\n          label: Etiketler\n          msg:\n            empty: Etiketler boş olamaz.\n        answer:\n          label: Cevap\n          msg:\n            empty: Cevap boş olamaz.\n        edit_summary:\n          label: Düzenleme özeti\n          placeholder: >-\n            Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi)\n    btn_post_question: Sorunuzu gönderin\n    btn_save_edits: Düzenlemeleri kaydet\n    answer_question: Kendi sorunuzu cevaplayın\n    post_question&answer: Sorunuzu ve cevabınızı gönderin\n  tag_selector:\n    add_btn: Etiket ekle\n    create_btn: Yeni etiket oluştur\n    search_tag: Etiket ara\n    hint: Sorunuzun ne hakkında olduğunu tanımlayın, en az bir etiket gereklidir.\n    hint_zero_tags: İçeriğinizin neyle ilgili olduğunu açıklayın.\n    hint_more_than_one_tag: \"İçeriğinizin neyle ilgili olduğunu açıklayın, en az {{min_tags_number}} etiket gereklidir.\"\n    no_result: Eşleşen etiket bulunamadı\n    tag_required_text: Gerekli etiket (en az bir tane)\n  header:\n    nav:\n      question: Sorular\n      tag: Etiketler\n      user: Kullanıcılar\n      badges: Rozetler\n      profile: Profil\n      setting: Ayarlar\n      logout: Çıkış yap\n      admin: Yönetici\n      review: İnceleme\n      bookmark: Yer İşaretleri\n      moderation: Moderasyon\n    search:\n      placeholder: Ara\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Değiştir\n    loading: yükleniyor...\n  pic_auth_code:\n    title: Captcha\n    placeholder: Yukarıdaki metni yazın\n    msg:\n      empty: Captcha boş olamaz.\n  inactive:\n    first: >-\n      Neredeyse tamamlandı! <bold>{{mail}}</bold> adresine bir aktivasyon e-postası gönderdik. Hesabınızı etkinleştirmek için lütfen e-postadaki talimatları izleyin.\n    info: \"E-posta gelmezse, spam klasörünüzü kontrol edin.\"\n    another: >-\n      <bold>{{mail}}</bold> adresine başka bir aktivasyon e-postası gönderdik. Gelmesi birkaç dakika sürebilir; spam klasörünüzü kontrol etmeyi unutmayın.\n    btn_name: Aktivasyon e-postasını yeniden gönder\n    change_btn_name: E-posta değiştir\n    msg:\n      empty: Boş olamaz.\n    resend_email:\n      url_label: Aktivasyon e-postasını yeniden göndermek istediğinizden emin misiniz?\n      url_text: Ayrıca yukarıdaki aktivasyon bağlantısını kullanıcıya verebilirsiniz.\n  login:\n    login_to_continue: Devam etmek için giriş yapın\n    info_sign: Hesabınız yok mu? <1>Kaydolun</1>\n    info_login: Zaten hesabınız var mı? <1>Giriş yapın</1>\n    agreements: Kaydolarak <1>gizlilik politikasını</1> ve <3>hizmet şartlarını</3> kabul etmiş olursunuz.\n    forgot_pass: Parolanızı mı unuttunuz?\n    name:\n      label: İsim\n      msg:\n        empty: İsim boş olamaz.\n        range: İsim 2 ile 30 karakter arasında olmalıdır.\n        character: 'Yalnızca \"a-z\", \"0-9\", \" - . _\" karakterleri kullanılabilir'\n    email:\n      label: E-posta\n      msg:\n        empty: E-posta boş olamaz.\n    password:\n      label: Parola\n      msg:\n        empty: Parola boş olamaz.\n        different: Her iki tarafta girilen parolalar tutarsız\n  account_forgot:\n    page_title: Parolanızı mı Unuttunuz\n    btn_name: Bana kurtarma e-postası gönder\n    send_success: >-\n      Eğer bir hesap <strong>{{mail}}</strong> ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız.\n    email:\n      label: E-posta\n      msg:\n        empty: E-posta boş olamaz.\n  change_email:\n    btn_cancel: İptal\n    btn_update: E-posta adresini güncelle\n    send_success: >-\n      Eğer bir hesap <strong>{{mail}}</strong> ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız.\n    email:\n      label: Yeni e-posta\n      msg:\n        empty: E-posta boş olamaz.\n  oauth:\n    connect: '{{auth_name}} ile bağlan'\n    remove: '{{auth_name}} kaldır'\n  oauth_bind_email:\n    subtitle: Hesabınıza bir kurtarma e-postası ekleyin.\n    btn_update: E-posta adresini güncelle\n    email:\n      label: E-posta\n      msg:\n        empty: E-posta boş olamaz.\n    modal_title: E-posta zaten mevcut.\n    modal_content: Bu e-posta adresi zaten kayıtlı. Mevcut hesaba bağlanmak istediğinizden emin misiniz?\n    modal_cancel: E-posta değiştir\n    modal_confirm: Mevcut hesaba bağlan\n  password_reset:\n    page_title: Parola Sıfırlama\n    btn_name: Parolamı sıfırla\n    reset_success: >-\n      Parolanızı başarıyla değiştirdiniz; giriş sayfasına yönlendirileceksiniz.\n    link_invalid: >-\n      Üzgünüz, bu parola sıfırlama bağlantısı artık geçerli değil. Belki de parolanız zaten sıfırlanmış?\n    to_login: Giriş sayfasına devam et\n    password:\n      label: Parola\n      msg:\n        empty: Parola boş olamaz.\n        length: Uzunluk 8 ile 32 arasında olmalıdır\n        different: Her iki tarafta girilen parolalar tutarsız\n    password_confirm:\n      label: Yeni parolayı onayla\n  settings:\n    page_title: Ayarlar\n    goto_modify: Değiştirmeye git\n    nav:\n      profile: Profil\n      notification: Bildirimler\n      account: Hesap\n      interface: Arayüz\n    profile:\n      heading: Profil\n      btn_name: Kaydet\n      display_name:\n        label: Görünen ad\n        msg: Görünen ad boş olamaz.\n        msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır.\n      username:\n        label: Kullanıcı adı\n        caption: İnsanlar size \"@kullaniciadi\" şeklinde bahsedebilir.\n        msg: Kullanıcı adı boş olamaz.\n        msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profil resmi\n        gravatar: Gravatar\n        gravatar_text: Resmi şurada değiştirebilirsiniz\n        custom: Özel\n        custom_text: Kendi resminizi yükleyebilirsiniz.\n        default: Sistem\n        msg: Lütfen bir avatar yükleyin\n      bio:\n        label: Hakkımda\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Website formatı yanlış\n      location:\n        label: Konum\n        placeholder: \"Şehir, Ülke\"\n    notification:\n      heading: E-posta Bildirimleri\n      turn_on: Aç\n      inbox:\n        label: Gelen kutusu bildirimleri\n        description: Sorularınıza cevaplar, yorumlar, davetler ve daha fazlası.\n      all_new_question:\n        label: Tüm yeni sorular\n        description: Tüm yeni sorulardan haberdar olun. Haftada en fazla 50 soru.\n      all_new_question_for_following_tags:\n        label: Takip edilen etiketler için tüm yeni sorular\n        description: Takip ettiğiniz etiketlerdeki yeni sorulardan haberdar olun.\n    account:\n      heading: Hesap\n      change_email_btn: E-posta değiştir\n      change_pass_btn: Parola değiştir\n      change_email_info: >-\n        Bu adrese bir e-posta gönderdik. Lütfen onay talimatlarını takip edin.\n      email:\n        label: E-posta\n      new_email:\n        label: Yeni e-posta\n        msg: Yeni e-posta boş olamaz.\n      pass:\n        label: Mevcut parola\n        msg: Parola boş olamaz.\n      password_title: Parola\n      current_pass:\n        label: Mevcut parola\n        msg:\n          empty: Mevcut parola boş olamaz.\n          length: Uzunluk 8 ile 32 arasında olmalıdır.\n          different: Girilen iki parola eşleşmiyor.\n      new_pass:\n        label: Yeni parola\n      pass_confirm:\n        label: Yeni parolayı onayla\n    interface:\n      heading: Arayüz\n      lang:\n        label: Arayüz dili\n        text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir.\n    my_logins:\n      title: Girişlerim\n      label: Bu hesapları kullanarak bu sitede giriş yapın veya kaydolun.\n      modal_title: Girişi kaldır\n      modal_content: Bu girişi hesabınızdan kaldırmak istediğinizden emin misiniz?\n      modal_confirm_btn: Kaldır\n      remove_success: Başarıyla kaldırıldı\n  toast:\n    update: güncelleme başarılı\n    update_password: Parola başarıyla değiştirildi.\n    flag_success: Bildirdiğiniz için teşekkürler.\n    forbidden_operate_self: Kendinizle ilgili işlem yapmak yasaktır\n    review: Revizyonunuz incelendikten sonra görünecek.\n    sent_success: Başarıyla gönderildi\n  related_question:\n    title: İle ilgili\n    answers: cevap\n  linked_question:\n    title: Bağlantılı\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: İnsanları Davet Et\n    desc: Cevap verebileceğini düşündüğünüz kişileri davet edin.\n    invite: Cevaplamaya davet et\n    add: Kişi ekle\n    search: Kişi ara\n  question_detail:\n    action: Eylem\n    created: Created\n    Asked: Soruldu\n    asked: sordu\n    update: Değiştirildi\n    Edited: Edited\n    edit: düzenledi\n    commented: yorum yaptı\n    Views: Görüntülendi\n    Follow: Takip Et\n    Following: Takip Ediliyor\n    follow_tip: Bildirim almak için bu soruyu takip edin\n    answered: cevapladı\n    closed_in: Şurada kapatıldı\n    show_exist: Var olan soruyu göster.\n    useful: Faydalı\n    question_useful: Faydalı ve açık\n    question_un_useful: Belirsiz veya faydalı değil\n    question_bookmark: Bu soruyu yer işaretlerine ekle\n    answer_useful: Faydalı\n    answer_un_useful: Faydalı değil\n    answers:\n      title: Cevaplar\n      score: Puan\n      newest: En Yeni\n      oldest: En Eski\n      btn_accept: Kabul Et\n      btn_accepted: Kabul Edildi\n    write_answer:\n      title: Cevabınız\n      edit_answer: Mevcut cevabımı düzenle\n      btn_name: Cevabınızı gönderin\n      add_another_answer: Başka bir cevap ekle\n      confirm_title: Cevaplamaya devam et\n      continue: Devam et\n      confirm_info: >-\n        <p>Başka bir cevap eklemek istediğinizden emin misiniz?</p><p>Bunun yerine mevcut cevabınızı iyileştirmek ve geliştirmek için düzenleme bağlantısını kullanabilirsiniz.</p>\n      empty: Cevap boş olamaz.\n      characters: içerik en az 6 karakter uzunluğunda olmalıdır.\n      tips:\n        header_1: Cevabınız için teşekkürler\n        li1_1: Lütfen <strong>soruyu cevapladığınızdan</strong> emin olun. Detaylar verin ve araştırmanızı paylaşın.\n        li1_2: Yaptığınız ifadeleri referanslar veya kişisel deneyimlerle destekleyin.\n        header_2: Ancak <strong>şunlardan kaçının</strong> ...\n        li2_1: Yardım istemek, açıklama istemek veya diğer cevaplara yanıt vermek.\n    reopen:\n      confirm_btn: Yeniden Aç\n      title: Bu gönderiyi yeniden aç\n      content: Yeniden açmak istediğinizden emin misiniz?\n    list:\n      confirm_btn: Listele\n      title: Bu gönderiyi listele\n      content: Listelemek istediğinizden emin misiniz?\n    unlist:\n      confirm_btn: Listeden Kaldır\n      title: Bu gönderiyi listeden kaldır\n      content: Listeden kaldırmak istediğinizden emin misiniz?\n    pin:\n      title: Bu gönderiyi sabitle\n      content: Küresel olarak sabitlemek istediğinizden emin misiniz? Bu gönderi tüm gönderi listelerinin en üstünde görünecektir.\n      confirm_btn: Sabitle\n  delete:\n    title: Bu gönderiyi sil\n    question: >-\n      <strong>Cevapları olan soruları silmenizi</strong> önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır.</p><p>Cevaplanmış soruları tekrar tekrar silmek, hesabınızın soru sorma yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz?\n    answer_accepted: >-\n      <p><strong>Kabul edilmiş cevabı silmenizi</strong> önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır. </p> Kabul edilmiş cevapları tekrar tekrar silmek, hesabınızın cevap verme yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz?\n    other: Silmek istediğinizden emin misiniz?\n    tip_answer_deleted: Bu cevap silinmiştir\n    undelete_title: Bu gönderinin silmesini geri al\n    undelete_desc: Silmeyi geri almak istediğinizden emin misiniz?\n  btns:\n    confirm: Onayla\n    cancel: İptal\n    edit: Düzenle\n    save: Kaydet\n    delete: Sil\n    undelete: Silmeyi Geri Al\n    list: Listele\n    unlist: Listeden Kaldır\n    unlisted: Listede Değil\n    login: Giriş Yap\n    signup: Kayıt Ol\n    logout: Çıkış Yap\n    verify: Doğrula\n    create: Oluştur\n    approve: Onayla\n    reject: Reddet\n    skip: Atla\n    discard_draft: Taslağı at\n    pinned: Sabitlendi\n    all: Tümü\n    question: Soru\n    answer: Cevap\n    comment: Yorum\n    refresh: Yenile\n    resend: Yeniden Gönder\n    deactivate: Devre Dışı Bırak\n    active: Aktif\n    suspend: Askıya Al\n    unsuspend: Askıyı Kaldır\n    close: Kapat\n    reopen: Yeniden Aç\n    ok: Tamam\n    light: Açık\n    dark: Koyu\n    system_setting: Sistem ayarı\n    default: Varsayılan\n    reset: Sıfırla\n    tag: Etiket\n    post_lowercase: gönderi\n    filter: Filtrele\n    ignore: Yoksay\n    submit: Gönder\n    normal: Normal\n    closed: Kapalı\n    deleted: Silindi\n    deleted_permanently: Kalıcı olarak silindi\n    pending: Beklemede\n    more: Daha Fazla\n    view: Görüntüle\n    card: Kart\n    compact: Kompakt\n    display_below: Aşağıda göster\n    always_display: Her zaman göster\n    or: veya\n    back_sites: Sitelere geri dön\n  search:\n    title: Arama Sonuçları\n    keywords: Anahtar Kelimeler\n    options: Seçenekler\n    follow: Takip Et\n    following: Takip Ediliyor\n    counts: \"{{count}} Sonuç\"\n    counts_loading: \"... Results\"\n    more: Daha Fazla\n    sort_btns:\n      relevance: İlgililik\n      newest: En Yeni\n      active: Aktif\n      score: Puan\n      more: Daha Fazla\n    tips:\n      title: Gelişmiş Arama İpuçları\n      tag: \"<1>[etiket]</1> bir etiketle ara\"\n      user: \"<1>user:kullanıcıadı</1> yazara göre ara\"\n      answer: \"<1>answers:0</1> cevaplanmamış sorular\"\n      score: \"<1>score:3</1> 3+ puana sahip gönderiler\"\n      question: \"<1>is:question</1> soruları ara\"\n      is_answer: \"<1>is:answer</1> cevapları ara\"\n    empty: Hiçbir şey bulamadık. <br /> Farklı veya daha az spesifik anahtar kelimeler deneyin.\n  share:\n    name: Paylaş\n    copy: Bağlantıyı kopyala\n    via: Gönderiyi şurada paylaş...\n    copied: Kopyalandı\n    facebook: Facebook'ta Paylaş\n    twitter: X'te Paylaş\n  cannot_vote_for_self: Kendi gönderinize oy veremezsiniz.\n  modal_confirm:\n    title: Hata...\n  delete_permanently:\n    title: Kalıcı olarak sil\n    content: Kalıcı olarak silmek istediğinizden emin misiniz?\n  account_result:\n    success: Yeni hesabınız onaylandı; ana sayfaya yönlendirileceksiniz.\n    link: Ana sayfaya devam et\n    oops: Hay aksi!\n    invalid: Kullandığınız bağlantı artık çalışmıyor.\n    confirm_new_email: E-postanız güncellendi.\n    confirm_new_email_invalid: >-\n      Üzgünüz, bu onay bağlantısı artık geçerli değil. Belki e-postanız zaten değiştirildi?\n  unsubscribe:\n    page_title: Abonelikten Çık\n    success_title: Abonelikten Çıkma Başarılı\n    success_desc: Bu abone listesinden başarıyla çıkarıldınız ve bizden başka e-posta almayacaksınız.\n    link: Ayarları değiştir\n  question:\n    following_tags: Takip Edilen Etiketler\n    edit: Düzenle\n    save: Kaydet\n    follow_tag_tip: Soru listenizi oluşturmak için etiketleri takip edin.\n    hot_questions: Popüler Sorular\n    all_questions: Tüm Sorular\n    x_questions: \"{{ count }} Soru\"\n    x_answers: \"{{ count }} cevap\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Sorular\n    answers: Cevaplar\n    newest: En Yeni\n    active: Aktif\n    hot: Popüler\n    frequent: Sık Sorulan\n    recommend: Önerilenler\n    score: Puan\n    unanswered: Cevaplanmamış\n    modified: değiştirildi\n    answered: cevaplandı\n    asked: soruldu\n    closed: kapandı\n    follow_a_tag: Bir etiketi takip et\n    more: Daha Fazla\n  personal:\n    overview: Genel Bakış\n    answers: Cevaplar\n    answer: cevap\n    questions: Sorular\n    question: soru\n    bookmarks: Yer İşaretleri\n    reputation: İtibar\n    comments: Yorumlar\n    votes: Oylar\n    badges: Rozetler\n    newest: En Yeni\n    score: Puan\n    edit_profile: Profili düzenle\n    visited_x_days: \"{{ count }} gün ziyaret edildi\"\n    viewed: Görüntülendi\n    joined: Katıldı\n    comma: \",\"\n    last_login: Görüldü\n    about_me: Hakkımda\n    about_me_empty: \"// Merhaba, Dünya !\"\n    top_answers: En İyi Cevaplar\n    top_questions: En İyi Sorular\n    stats: İstatistikler\n    list_empty: Gönderi bulunamadı.<br />Belki farklı bir sekme seçmek istersiniz?\n    content_empty: Gönderi bulunamadı.\n    accepted: Kabul Edildi\n    answered: cevaplandı\n    asked: sordu\n    downvoted: negatif oylandı\n    mod_short: MOD\n    mod_long: Moderatörler\n    x_reputation: itibar\n    x_votes: alınan oy\n    x_answers: cevap\n    x_questions: soru\n    recent_badges: Son Rozetler\n  install:\n    title: Kurulum\n    next: İleri\n    done: Tamamlandı\n    config_yaml_error: config.yaml dosyası oluşturulamıyor.\n    lang:\n      label: Lütfen bir dil seçin\n    db_type:\n      label: Veritabanı motoru\n    db_username:\n      label: Kullanıcı adı\n      placeholder: root\n      msg: Kullanıcı adı boş olamaz.\n    db_password:\n      label: Parola\n      placeholder: root\n      msg: Parola boş olamaz.\n    db_host:\n      label: Veritabanı sunucusu\n      placeholder: \"db:3306\"\n      msg: Veritabanı sunucusu boş olamaz.\n    db_name:\n      label: Veritabanı adı\n      placeholder: answer\n      msg: Veritabanı adı boş olamaz.\n    db_file:\n      label: Veritabanı dosyası\n      placeholder: /data/answer.db\n      msg: Veritabanı dosyası boş olamaz.\n    ssl_enabled:\n      label: SSL'i Etkinleştir\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Modu\n    ssl_root_cert:\n      placeholder: sslrootcert dosya yolu\n      msg: sslrootcert dosya yolu boş olamaz\n    ssl_cert:\n      placeholder: sslcert dosya yolu\n      msg: sslcert dosya yolu boş olamaz\n    ssl_key:\n      placeholder: sslkey dosya yolu\n      msg: sslkey dosya yolu boş olamaz\n    config_yaml:\n      title: config.yaml Oluştur\n      label: config.yaml dosyası oluşturuldu.\n      desc: >-\n        <1>config.yaml</1> dosyasını <1>/var/wwww/xxx/</1> dizininde manuel olarak oluşturabilir ve aşağıdaki metni içine yapıştırabilirsiniz.\n      info: Bunu yaptıktan sonra \"İleri\" düğmesine tıklayın.\n    site_information: Site Bilgisi\n    admin_account: Yönetici Hesabı\n    site_name:\n      label: Site adı\n      msg: Saat dilimi boş olamaz.\n      msg_max_length: Site adı en fazla 30 karakter uzunluğunda olmalıdır.\n    site_url:\n      label: Site URL'si\n      text: Sitenizin adresi.\n      msg:\n        empty: Site URL'si boş olamaz.\n        incorrect: Site URL'si formatı yanlış.\n        max_length: Site URL'si en fazla 512 karakter uzunluğunda olmalıdır.\n    contact_email:\n      label: İletişim e-postası\n      text: Bu siteden sorumlu kilit kişinin e-posta adresi.\n      msg:\n        empty: İletişim e-postası boş olamaz.\n        incorrect: İletişim e-postası formatı yanlış.\n    login_required:\n      label: Özel\n      switch: Giriş gerekli\n      text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir.\n    admin_name:\n      label: İsim\n      msg: İsim boş olamaz.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: İsim 2 ile 30 karakter arasında olmalıdır.\n    admin_password:\n      label: Parola\n      text: >-\n        Giriş yapmak için bu parolaya ihtiyacınız olacak. Lütfen güvenli bir yerde saklayın.\n      msg: Parola boş olamaz.\n      msg_min_length: Parola en az 8 karakter uzunluğunda olmalıdır.\n      msg_max_length: Parola en fazla 32 karakter uzunluğunda olmalıdır.\n    admin_confirm_password:\n      label: \"Parolayı Onayla\"\n      text: \"Lütfen onaylamak için parolanızı tekrar girin.\"\n      msg: \"Onay parolası eşleşmiyor.\"\n    admin_email:\n      label: E-posta\n      text: Giriş yapmak için bu e-postaya ihtiyacınız olacak.\n      msg:\n        empty: E-posta boş olamaz.\n        incorrect: E-posta formatı yanlış.\n    ready_title: Siteniz hazır\n    ready_desc: >-\n      Daha fazla ayarı değiştirmek isterseniz, <1>yönetici bölümünü</1> ziyaret edin; site menüsünde bulabilirsiniz.\n    good_luck: \"İyi eğlenceler ve iyi şanslar!\"\n    warn_title: Uyarı\n    warn_desc: >-\n      <1>config.yaml</1> dosyası zaten var. Bu dosyadaki herhangi bir yapılandırma öğesini sıfırlamanız gerekiyorsa, lütfen önce dosyayı silin.\n    install_now: <1>Şimdi kurulum</1> yapmayı deneyebilirsiniz.\n    installed: Zaten kurulu\n    installed_desc: >-\n      Zaten kurulum yapmış görünüyorsunuz. Yeniden kurmak için lütfen önce eski veritabanı tablolarınızı temizleyin.\n    db_failed: Veritabanı bağlantısı başarısız\n    db_failed_desc: >-\n      Bu, <1>config.yaml</1> dosyanızdaki veritabanı bilgilerinin yanlış olduğu veya veritabanı sunucusuyla bağlantı kurulamadığı anlamına gelir. Bu, sunucunuzun veritabanı sunucusunun çalışmadığı anlamına gelebilir.\n  counts:\n    views: görüntülenme\n    votes: oy\n    answers: cevap\n    accepted: Kabul Edildi\n  page_error:\n    http_error: HTTP Hatası {{ code }}\n    desc_403: Bu sayfaya erişim izniniz yok.\n    desc_404: Maalesef, bu sayfa mevcut değil.\n    desc_50X: Sunucu bir hatayla karşılaştı ve isteğinizi tamamlayamadı.\n    back_home: Ana sayfaya dön\n  page_maintenance:\n    desc: \"Bakım altındayız, yakında geri döneceğiz.\"\n  nav_menus:\n    dashboard: Gösterge Paneli\n    contents: İçerikler\n    questions: Sorular\n    answers: Cevaplar\n    users: Kullanıcılar\n    badges: Rozetler\n    flags: Bildirimler\n    settings: Ayarlar\n    general: Genel\n    interface: Arayüz\n    smtp: SMTP\n    branding: Marka\n    legal: Yasal\n    write: Yaz\n    terms: Terms\n    tos: Kullanım Şartları\n    privacy: Gizlilik\n    seo: SEO\n    customize: Özelleştir\n    themes: Temalar\n    login: Giriş\n    privileges: Ayrıcalıklar\n    plugins: Eklentiler\n    installed_plugins: Kurulu Eklentiler\n    apperance: Görünüm\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: '{{site_name}} sitesine hoş geldiniz'\n  user_center:\n    login: Giriş\n    qrcode_login_tip: Lütfen QR kodu taramak ve giriş yapmak için {{ agentName }} kullanın.\n    login_failed_email_tip: Giriş başarısız oldu, lütfen tekrar denemeden önce bu uygulamanın e-posta bilgilerinize erişmesine izin verin.\n  badges:\n    modal:\n      title: Tebrikler\n      content: Yeni bir rozet kazandınız.\n      close: Kapat\n      confirm: Rozetleri görüntüle\n    title: Rozetler\n    awarded: Kazanıldı\n    earned_×: '{{ number }} kez kazanıldı'\n    ×_awarded: \"{{ number }} kez verildi\"\n    can_earn_multiple: Bunu birden çok kez kazanabilirsiniz.\n    earned: Kazanıldı\n  admin:\n    admin_header:\n      title: Yönetici\n    dashboard:\n      title: Gösterge Paneli\n      welcome: Yönetici'ye Hoş Geldiniz!\n      site_statistics: Site istatistikleri\n      questions: \"Sorular:\"\n      resolved: \"Çözülmüş:\"\n      unanswered: \"Cevaplanmamış:\"\n      answers: \"Cevaplar:\"\n      comments: \"Yorumlar:\"\n      votes: \"Oylar:\"\n      users: \"Kullanıcılar:\"\n      flags: \"Bildirimler:\"\n      reviews: \"İncelemeler:\"\n      site_health: Site sağlığı\n      version: \"Sürüm:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Yükleme klasörü:\"\n      run_mode: \"Çalışma modu:\"\n      private: Özel\n      public: Herkese Açık\n      smtp: \"SMTP:\"\n      timezone: \"Saat dilimi:\"\n      system_info: Sistem bilgisi\n      go_version: \"Go sürümü:\"\n      database: \"Veritabanı:\"\n      database_size: \"Veritabanı boyutu:\"\n      storage_used: \"Kullanılan depolama:\"\n      uptime: \"Çalışma süresi:\"\n      links: Bağlantılar\n      plugins: Eklentiler\n      github: GitHub\n      blog: Blog\n      contact: İletişim\n      forum: Forum\n      documents: Belgeler\n      feedback: Geribildirim\n      support: Destek\n      review: İnceleme\n      config: Yapılandırma\n      update_to: Güncelle\n      latest: En son\n      check_failed: Kontrol başarısız\n      \"yes\": \"Evet\"\n      \"no\": \"Hayır\"\n      not_allowed: İzin verilmiyor\n      allowed: İzin veriliyor\n      enabled: Etkin\n      disabled: Devre dışı\n      writable: Yazılabilir\n      not_writable: Yazılamaz\n    flags:\n      title: Bildirimler\n      pending: Bekleyen\n      completed: Tamamlanan\n      flagged: Bildirilen\n      flagged_type: Bildirilen {{ type }}\n      created: Oluşturuldu\n      action: Eylem\n      review: İnceleme\n    user_role_modal:\n      title: Kullanıcı rolünü şuna değiştir...\n      btn_cancel: İptal\n      btn_submit: Gönder\n    new_password_modal:\n      title: Yeni parola belirle\n      form:\n        fields:\n          password:\n            label: Parola\n            text: Kullanıcının oturumu kapatılacak ve tekrar giriş yapması gerekecek.\n            msg: Parola 8-32 karakter uzunluğunda olmalıdır.\n      btn_cancel: İptal\n      btn_submit: Gönder\n    edit_profile_modal:\n      title: Profili düzenle\n      form:\n        fields:\n          display_name:\n            label: Görünen ad\n            msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır.\n          username:\n            label: Kullanıcı adı\n            msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır.\n          email:\n            label: E-posta\n            msg_invalid: Geçersiz E-posta Adresi.\n      edit_success: Başarıyla düzenlendi\n      btn_cancel: İptal\n      btn_submit: Gönder\n    user_modal:\n      title: Yeni kullanıcı ekle\n      form:\n        fields:\n          users:\n            label: Toplu kullanıcı ekle\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: İsim, e-posta, parola bilgilerini virgülle ayırın. Her satırda bir kullanıcı.\n            msg: \"Lütfen kullanıcının e-postasını girin, her satırda bir tane.\"\n          display_name:\n            label: Görünen ad\n            msg: Görünen ad 2-30 karakter uzunluğunda olmalıdır.\n          email:\n            label: E-posta\n            msg: E-posta geçerli değil.\n          password:\n            label: Parola\n            msg: Parola 8-32 karakter uzunluğunda olmalıdır.\n      btn_cancel: İptal\n      btn_submit: Gönder\n    users:\n      title: Kullanıcılar\n      name: İsim\n      email: E-posta\n      reputation: İtibar\n      created_at: Oluşturulma zamanı\n      delete_at: Silinme zamanı\n      suspend_at: Askıya Alınma Zamanı\n      suspend_until: Suspend until\n      status: Durum\n      role: Rol\n      action: Eylem\n      change: Değiştir\n      all: Tümü\n      staff: Ekip\n      more: Daha Fazla\n      inactive: Etkin Değil\n      suspended: Askıya Alınmış\n      deleted: Silinmiş\n      normal: Normal\n      Moderator: Moderatör\n      Admin: Yönetici\n      User: Kullanıcı\n      filter:\n        placeholder: \"İsme göre filtreleme, user:id\"\n      set_new_password: Yeni parola belirle\n      edit_profile: Profili düzenle\n      change_status: Durumu değiştir\n      change_role: Rolü değiştir\n      show_logs: Kayıtları göster\n      add_user: Kullanıcı ekle\n      deactivate_user:\n        title: Kullanıcıyı devre dışı bırak\n        content: Etkin olmayan bir kullanıcının e-postasını yeniden doğrulaması gerekir.\n      delete_user:\n        title: Bu kullanıcıyı sil\n        content: Bu kullanıcıyı silmek istediğinizden emin misiniz? Bu kalıcıdır!\n        remove: İçeriklerini kaldır\n        label: Tüm soruları, cevapları, yorumları vb. kaldır.\n        text: Sadece kullanıcının hesabını silmek istiyorsanız bunu işaretlemeyin.\n      suspend_user:\n        title: Bu kullanıcıyı askıya al\n        content: Askıya alınmış bir kullanıcı giriş yapamaz.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Sorular\n      unlisted: Listelenmemiş\n      post: Gönderi\n      votes: Oylar\n      answers: Cevaplar\n      created: Oluşturuldu\n      status: Durum\n      action: Eylem\n      change: Değiştir\n      pending: Beklemede\n      filter:\n        placeholder: \"Başlığa göre filtreleme, question:id\"\n    answers:\n      page_title: Cevaplar\n      post: Gönderi\n      votes: Oylar\n      created: Oluşturuldu\n      status: Durum\n      action: Eylem\n      change: Değiştir\n      filter:\n        placeholder: \"Başlığa göre filtreleme, answer:id\"\n    general:\n      page_title: Genel\n      name:\n        label: Site adı\n        msg: Site adı boş olamaz.\n        text: \"Başlık etiketinde kullanılan bu sitenin adı.\"\n      site_url:\n        label: Site URL'si\n        msg: Site url'si boş olamaz.\n        validate: Lütfen geçerli bir URL girin.\n        text: Sitenizin adresi.\n      short_desc:\n        label: Kısa site açıklaması\n        msg: Kısa site açıklaması boş olamaz.\n        text: \"Ana sayfadaki başlık etiketinde kullanılan kısa açıklama.\"\n      desc:\n        label: Site açıklaması\n        msg: Site açıklaması boş olamaz.\n        text: \"Bu siteyi bir cümleyle açıklayın, meta açıklama etiketinde kullanılır.\"\n      contact_email:\n        label: İletişim e-postası\n        msg: İletişim e-postası boş olamaz.\n        validate: İletişim e-postası geçerli değil.\n        text: Bu siteden sorumlu kilit kişinin e-posta adresi.\n      check_update:\n        label: Yazılım güncellemeleri\n        text: Güncellemeleri otomatik olarak kontrol et\n    interface:\n      page_title: Arayüz\n      language:\n        label: Arayüz dili\n        msg: Arayüz dili boş olamaz.\n        text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir.\n      time_zone:\n        label: Saat dilimi\n        msg: Saat dilimi boş olamaz.\n        text: Sizinle aynı saat dilimindeki bir şehri seçin.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Gönderen e-posta\n        msg: Gönderen e-posta boş olamaz.\n        text: E-postaların gönderildiği e-posta adresi.\n      from_name:\n        label: Gönderen adı\n        msg: Gönderen adı boş olamaz.\n        text: E-postaların gönderildiği isim.\n      smtp_host:\n        label: SMTP sunucusu\n        msg: SMTP sunucusu boş olamaz.\n        text: Mail sunucunuz.\n      encryption:\n        label: Şifreleme\n        msg: Şifreleme boş olamaz.\n        text: Çoğu sunucu için SSL önerilen seçenektir.\n        ssl: SSL\n        tls: TLS\n        none: Yok\n      smtp_port:\n        label: SMTP portu\n        msg: SMTP portu 1 ~ 65535 arasında bir sayı olmalıdır.\n        text: Mail sunucunuzun portu.\n      smtp_username:\n        label: SMTP kullanıcı adı\n        msg: SMTP kullanıcı adı boş olamaz.\n      smtp_password:\n        label: SMTP parolası\n        msg: SMTP parolası boş olamaz.\n      test_email_recipient:\n        label: Test e-posta alıcıları\n        text: Test gönderimlerini alacak e-posta adresini girin.\n        msg: Test e-posta alıcıları geçersiz\n      smtp_authentication:\n        label: Kimlik doğrulamayı etkinleştir\n        title: SMTP kimlik doğrulaması\n        msg: SMTP kimlik doğrulaması boş olamaz.\n        \"yes\": \"Evet\"\n        \"no\": \"Hayır\"\n    branding:\n      page_title: Marka\n      logo:\n        label: Logo\n        msg: Logo boş olamaz.\n        text: Sitenizin sol üst köşesindeki logo resmi. 56 yüksekliğinde ve 3:1'den büyük en-boy oranlı geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, site başlık metni gösterilecektir.\n      mobile_logo:\n        label: Mobil logo\n        text: Sitenizin mobil versiyonunda kullanılan logo. 56 yüksekliğinde geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, \"logo\" ayarındaki resim kullanılacaktır.\n      square_icon:\n        label: Kare simge\n        msg: Kare simge boş olamaz.\n        text: Meta veri simgeleri için temel olarak kullanılan resim. İdeal olarak 512x512'den büyük olmalıdır.\n      favicon:\n        label: Favicon\n        text: Siteniz için bir favicon. CDN üzerinde düzgün çalışması için png olmalıdır. 32x32 boyutuna yeniden boyutlandırılacaktır. Boş bırakılırsa, \"kare simge\" kullanılacaktır.\n    legal:\n      page_title: Yasal\n      terms_of_service:\n        label: Kullanım şartları\n        text: \"Buraya kullanım şartları içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin.\"\n      privacy_policy:\n        label: Gizlilik politikası\n        text: \"Buraya gizlilik politikası içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin.\"\n      external_content_display:\n        label: Harici içerik\n        text: \"İçerik, harici web sitelerinden gömülen resimler, videolar ve medyayı içerir.\"\n        always_display: Her zaman harici içeriği göster\n        ask_before_display: Harici içeriği göstermeden önce sor\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Cevap yazma\n        label: Her kullanıcı aynı soru için sadece bir cevap yazabilir\n        text: \"Kullanıcıların aynı soruya birden fazla cevap yazmasına izin vermek için kapatın, bu cevapların odaktan uzaklaşmasına neden olabilir.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Önerilen etiketler\n        text: \"Önerilen etiketler varsayılan olarak açılır listede gösterilecektir.\"\n        msg:\n          contain_reserved: \"önerilen etiketler ayrılmış etiketleri içeremez\"\n      required_tag:\n        title: Gerekli etiketleri ayarla\n        label: Önerilen etiketleri gerekli etiketler olarak ayarla\n        text: \"Her yeni soru en az bir önerilen etikete sahip olmalıdır.\"\n      reserved_tags:\n        label: Ayrılmış etiketler\n        text: \"Ayrılmış etiketler sadece moderatör tarafından kullanılabilir.\"\n      image_size:\n        label: Maksimum resim boyutu (MB)\n        text: \"Maksimum resim yükleme boyutu.\"\n      attachment_size:\n        label: Maksimum ek dosya boyutu (MB)\n        text: \"Maksimum ek dosya yükleme boyutu.\"\n      image_megapixels:\n        label: Maksimum resim megapikseli\n        text: \"Bir resim için izin verilen maksimum megapiksel sayısı.\"\n      image_extensions:\n        label: İzin verilen resim uzantıları\n        text: \"Resim gösterimi için izin verilen dosya uzantılarının listesi, virgülle ayırın.\"\n      attachment_extensions:\n        label: İzin verilen ek dosya uzantıları\n        text: \"Yükleme için izin verilen dosya uzantılarının listesi, virgülle ayırın. UYARI: Yüklemelere izin vermek güvenlik sorunlarına neden olabilir.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Kalıcı bağlantı\n        text: Özel URL yapıları, bağlantılarınızın kullanılabilirliğini ve ileriye dönük uyumluluğunu iyileştirebilir.\n      robots:\n        label: robots.txt\n        text: Bu, ilgili tüm site ayarlarını kalıcı olarak geçersiz kılacaktır.\n    themes:\n      page_title: Temalar\n      themes:\n        label: Temalar\n        text: Mevcut bir tema seçin.\n      color_scheme:\n        label: Renk şeması\n      navbar_style:\n        label: Gezinme çubuğu arka plan stili\n      primary_color:\n        label: Ana renk\n        text: Temalarınızda kullanılan renkleri değiştirin\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS ve HTML\n      custom_css:\n        label: Özel CSS\n        text: >\n          Bu &lt;link> olarak eklenecektir\n      head:\n        label: Head\n        text: >\n          Bu &lt;/head> öncesine eklenecektir\n      header:\n        label: Header\n        text: >\n          Bu &lt;body> sonrasına eklenecektir\n      footer:\n        label: Footer\n        text: Bu &lt;/body> öncesine eklenecektir\n      sidebar:\n        label: Kenar çubuğu\n        text: Bu kenar çubuğuna eklenecektir.\n    login:\n      page_title: Giriş\n      membership:\n        title: Üyelik\n        label: Yeni kayıtlara izin ver\n        text: Herhangi birinin yeni hesap oluşturmasını engellemek için kapatın.\n      email_registration:\n        title: E-posta kaydı\n        label: E-posta kaydına izin ver\n        text: Herhangi birinin e-posta yoluyla yeni hesap oluşturmasını engellemek için kapatın.\n      allowed_email_domains:\n        title: İzin verilen e-posta alan adları\n        text: Kullanıcıların hesap kaydı yapması gereken e-posta alan adları. Her satırda bir alan adı. Boş olduğunda dikkate alınmaz.\n      private:\n        title: Özel\n        label: Giriş gerekli\n        text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir.\n      password_login:\n        title: Parola ile giriş\n        label: E-posta ve parola ile girişe izin ver\n        text: \"UYARI: Kapatırsanız, daha önce başka bir giriş yöntemi yapılandırmadıysanız giriş yapamayabilirsiniz.\"\n    installed_plugins:\n      title: Kurulu Eklentiler\n      plugin_link: Eklentiler işlevselliği genişletir ve artırır. Eklentileri <1>Eklenti Deposu</1>'nda bulabilirsiniz.\n      filter:\n        all: Tümü\n        active: Aktif\n        inactive: Pasif\n        outdated: Güncel değil\n      plugins:\n        label: Eklentiler\n        text: Mevcut bir eklenti seçin.\n      name: İsim\n      version: Sürüm\n      status: Durum\n      action: Eylem\n      deactivate: Devre dışı bırak\n      activate: Etkinleştir\n      settings: Ayarlar\n    settings_users:\n      title: Kullanıcılar\n      avatar:\n        label: Varsayılan avatar\n        text: Kendi özel avatarı olmayan kullanıcılar için.\n      gravatar_base_url:\n        label: Gravatar temel URL'si\n        text: Gravatar sağlayıcısının API temel URL'si. Boş olduğunda dikkate alınmaz.\n      profile_editable:\n        title: Profil düzenlenebilir\n      allow_update_display_name:\n        label: Kullanıcıların görünen adlarını değiştirmelerine izin ver\n      allow_update_username:\n        label: Kullanıcıların kullanıcı adlarını değiştirmelerine izin ver\n      allow_update_avatar:\n        label: Kullanıcıların profil resimlerini değiştirmelerine izin ver\n      allow_update_bio:\n        label: Kullanıcıların hakkında bilgilerini değiştirmelerine izin ver\n      allow_update_website:\n        label: Kullanıcıların web sitelerini değiştirmelerine izin ver\n      allow_update_location:\n        label: Kullanıcıların konumlarını değiştirmelerine izin ver\n    privilege:\n      title: Ayrıcalıklar\n      level:\n        label: Gereken itibar seviyesi\n        text: Ayrıcalıklar için gereken itibarı seçin\n      msg:\n        should_be_number: giriş bir sayı olmalıdır\n        number_larger_1: sayı 1'e eşit veya daha büyük olmalıdır\n    badges:\n      action: Eylem\n      active: Aktif\n      activate: Etkinleştir\n      all: Tümü\n      awards: Ödüller\n      deactivate: Devre dışı bırak\n      filter:\n        placeholder: İsme göre filtreleme, badge:id\n      group: Grup\n      inactive: Pasif\n      name: İsim\n      show_logs: Kayıtları göster\n      status: Durum\n      title: Rozetler\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (isteğe bağlı)\n    empty: boş olamaz\n    invalid: geçersiz\n    btn_submit: Kaydet\n    not_found_props: \"Gerekli {{ key }} özelliği bulunamadı.\"\n    select: Seç\n  page_review:\n    review: İnceleme\n    proposed: önerilen\n    question_edit: Soru düzenleme\n    answer_edit: Cevap düzenleme\n    tag_edit: Etiket düzenleme\n    edit_summary: Düzenleme özeti\n    edit_question: Soruyu düzenle\n    edit_answer: Cevabı düzenle\n    edit_tag: Etiketi düzenle\n    empty: İncelenecek görev kalmadı.\n    approve_revision_tip: Bu revizyonu onaylıyor musunuz?\n    approve_flag_tip: Bu bildirimi onaylıyor musunuz?\n    approve_post_tip: Bu gönderiyi onaylıyor musunuz?\n    approve_user_tip: Bu kullanıcıyı onaylıyor musunuz?\n    suggest_edits: Önerilen düzenlemeler\n    flag_post: Gönderiyi bildir\n    flag_user: Kullanıcıyı bildir\n    queued_post: Sıradaki gönderi\n    queued_user: Sıradaki kullanıcı\n    filter_label: Tür\n    reputation: itibar\n    flag_post_type: Bu gönderiyi {{ type }} olarak bildirdi.\n    flag_user_type: Bu kullanıcıyı {{ type }} olarak bildirdi.\n    edit_post: Gönderiyi düzenle\n    list_post: Gönderiyi listele\n    unlist_post: Gönderiyi listeden kaldır\n  timeline:\n    undeleted: silme geri alındı\n    deleted: silindi\n    downvote: negatif oy\n    upvote: pozitif oy\n    accept: kabul et\n    cancelled: iptal edildi\n    commented: yorum yapıldı\n    rollback: geri alındı\n    edited: düzenlendi\n    answered: cevaplandı\n    asked: soruldu\n    closed: kapatıldı\n    reopened: yeniden açıldı\n    created: oluşturuldu\n    pin: sabitlendi\n    unpin: sabitlenme kaldırıldı\n    show: listelendi\n    hide: listelenmedi\n    title: \"Geçmiş:\"\n    tag_title: \"Zaman çizelgesi:\"\n    show_votes: \"Oyları göster\"\n    n_or_a: Yok\n    title_for_question: \"Zaman çizelgesi:\"\n    title_for_answer: \"{{ author }} tarafından {{ title }} sorusuna verilen cevabın zaman çizelgesi\"\n    title_for_tag: \"Etiket için zaman çizelgesi\"\n    datetime: Tarih/Saat\n    type: Tür\n    by: Yapan\n    comment: Yorum\n    no_data: \"Hiçbir şey bulamadık.\"\n  users:\n    title: Kullanıcılar\n    users_with_the_most_reputation: Bu hafta en yüksek itibar puanına sahip kullanıcılar\n    users_with_the_most_vote: Bu hafta en çok oy veren kullanıcılar\n    staffs: Topluluk ekibimiz\n    reputation: itibar\n    votes: oy\n  prompt:\n    leave_page: Sayfadan ayrılmak istediğinizden emin misiniz?\n    changes_not_save: Değişiklikleriniz kaydedilmeyebilir.\n  draft:\n    discard_confirm: Taslağınızı atmak istediğinizden emin misiniz?\n  messages:\n    post_deleted: Bu gönderi silindi.\n    post_cancel_deleted: Bu gönderinin silme işlemi geri alındı.\n    post_pin: Bu gönderi sabitlendi.\n    post_unpin: Bu gönderinin sabitlenmesi kaldırıldı.\n    post_hide_list: Bu gönderi listede gizlendi.\n    post_show_list: Bu gönderi listede gösterildi.\n    post_reopen: Bu gönderi yeniden açıldı.\n    post_list: Bu gönderi listelendi.\n    post_unlist: Bu gönderi listeden kaldırıldı.\n    post_pending: Gönderiniz inceleme bekliyor. Bu bir önizlemedir, onaylandıktan sonra görünür olacaktır.\n    post_closed: Bu gönderi kapatıldı.\n    answer_deleted: Bu cevap silindi.\n    answer_cancel_deleted: Bu cevabın silme işlemi geri alındı.\n    change_user_role: Bu kullanıcının rolü değiştirildi.\n    user_inactive: Bu kullanıcı zaten etkin değil.\n    user_normal: Bu kullanıcı zaten normal durumda.\n    user_suspended: Bu kullanıcı askıya alındı.\n    user_deleted: Bu kullanıcı silindi.\n    user_added: User has been added successfully.\n    badge_activated: Bu rozet etkinleştirildi.\n    badge_inactivated: Bu rozet devre dışı bırakıldı.\n    users_deleted: Bu kullanıcılar silindi.\n    posts_deleted: Bu sorular silindi.\n    answers_deleted: Bu cevaplar silindi.\n    copy: Panoya kopyala\n    copied: Kopyalandı\n    external_content_warning: Harici resimler/medya gösterilmiyor.\n\n\n"
  },
  {
    "path": "i18n/uk_UA.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Успішно.\n    unknown:\n      other: Невідома помилка.\n    request_format_error:\n      other: Неприпустимий формат запиту.\n    unauthorized_error:\n      other: Не авторизовано.\n    database_error:\n      other: Помилка сервера даних.\n    forbidden_error:\n      other: Заборонено.\n    duplicate_request_error:\n      other: Повторний запит.\n  action:\n    report:\n      other: Відмітити\n    edit:\n      other: Редагувати\n    delete:\n      other: Видалити\n    close:\n      other: Закрити\n    reopen:\n      other: Відкрити знову\n    forbidden_error:\n      other: Заборонено.\n    pin:\n      other: Закріпити\n    hide:\n      other: Вилучити зі списку\n    unpin:\n      other: Відкріпити\n    show:\n      other: Список\n    invite_someone_to_answer:\n      other: Редагувати\n    undelete:\n      other: Скасувати видалення\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Користувач\n      admin:\n        other: Адмін\n      moderator:\n        other: Модератор\n    description:\n      user:\n        other: За замовчуванням без спеціального доступу.\n      admin:\n        other: Має повний доступ до сайту.\n      moderator:\n        other: Має доступ до всіх дописів, окрім налаштувань адміністратора.\n  privilege:\n    level_1:\n      description:\n        other: Рівень 1 (для приватної команди, групи потрібна менша репутація)\n    level_2:\n      description:\n        other: Рівень 2 (низька репутація, необхідна для стартап-спільноти)\n    level_3:\n      description:\n        other: Рівень 3 (висока репутація, необхідна для зрілої спільноти)\n    level_custom:\n      description:\n        other: Користувацький рівень\n    rank_question_add_label:\n      other: Задати питання\n    rank_answer_add_label:\n      other: Написати відповідь\n    rank_comment_add_label:\n      other: Написати коментар\n    rank_report_add_label:\n      other: Відмітити\n    rank_comment_vote_up_label:\n      other: Проголосувати за коментар\n    rank_link_url_limit_label:\n      other: Публікуйте більш ніж 2 посилання одночасно\n    rank_question_vote_up_label:\n      other: Проголосувати за питання\n    rank_answer_vote_up_label:\n      other: Проголосувати за відповідь\n    rank_question_vote_down_label:\n      other: Проголосувати проти питання\n    rank_answer_vote_down_label:\n      other: Проголосувати проти відповіді\n    rank_invite_someone_to_answer_label:\n      other: Запросити когось відповісти\n    rank_tag_add_label:\n      other: Створити новий теґ\n    rank_tag_edit_label:\n      other: Редагувати опис теґу (необхідно розглянути)\n    rank_question_edit_label:\n      other: Редагувати чуже питання (необхідно розглянути)\n    rank_answer_edit_label:\n      other: Редагувати чужу відповідь (необхідно розглянути)\n    rank_question_edit_without_review_label:\n      other: Редагувати чуже питання без розгляду\n    rank_answer_edit_without_review_label:\n      other: Редагувати чужу відповідь без розгляду\n    rank_question_audit_label:\n      other: Переглянути редагування питання\n    rank_answer_audit_label:\n      other: Переглянути редагування відповіді\n    rank_tag_audit_label:\n      other: Переглянути редагування теґу\n    rank_tag_edit_without_review_label:\n      other: Редагувати опис теґу без розгляду\n    rank_tag_synonym_label:\n      other: Керування синонімами тегів\n  email:\n    other: Електронна пошта\n  e_mail:\n    other: Електронна пошта\n  password:\n    other: Пароль\n  pass:\n    other: Пароль\n  old_pass:\n    other: Current password\n  original_text:\n    other: Цей допис\n  email_or_password_wrong_error:\n    other: Електронна пошта та пароль не збігаються.\n  error:\n    common:\n      invalid_url:\n        other: Невірна URL.\n      status_invalid:\n        other: Неприпустимий статус.\n    password:\n      space_invalid:\n        other: Пароль не може містити пробіли.\n    admin:\n      cannot_update_their_password:\n        other: Ви не можете змінити свій пароль.\n      cannot_edit_their_profile:\n        other: Ви не можете змінити свій профіль.\n      cannot_modify_self_status:\n        other: Ви не можете змінити свій статус.\n      email_or_password_wrong:\n        other: Електронна пошта та пароль не збігаються.\n    answer:\n      not_found:\n        other: Відповідь не знайдено.\n      cannot_deleted:\n        other: Немає дозволу на видалення.\n      cannot_update:\n        other: Немає дозволу на оновлення.\n      question_closed_cannot_add:\n        other: Питання закриті й не можуть бути додані.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Коментарі не можна редагувати.\n      not_found:\n        other: Коментар не знайдено.\n      cannot_edit_after_deadline:\n        other: Час коментаря був занадто довгим, щоб його можна було змінити.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Такий E-mail вже існує.\n      need_to_be_verified:\n        other: Електронна пошта повинна бути підтверджена.\n      verify_url_expired:\n        other: Термін дії підтвердженої URL-адреси закінчився, будь ласка, надішліть листа повторно.\n      illegal_email_domain_error:\n        other: З цього поштового домену заборонено надсилати електронну пошту. Будь ласка, використовуйте інший.\n    lang:\n      not_found:\n        other: Мовний файл не знайдено.\n    object:\n      captcha_verification_failed:\n        other: Неправильно введено капчу.\n      disallow_follow:\n        other: Вам не дозволено підписатися.\n      disallow_vote:\n        other: Вам не дозволено голосувати.\n      disallow_vote_your_self:\n        other: Ви не можете проголосувати за власну публікацію.\n      not_found:\n        other: Обʼєкт не знайдено.\n      verification_failed:\n        other: Не вдалося виконати перевірку.\n      email_or_password_incorrect:\n        other: Електронна пошта та пароль не збігаються.\n      old_password_verification_failed:\n        other: Не вдалося перевірити старий пароль\n      new_password_same_as_previous_setting:\n        other: Новий пароль збігається з попереднім.\n      already_deleted:\n        other: Публікацію видалено.\n    meta:\n      object_not_found:\n        other: Мета-об'єкт не знайдено\n    question:\n      already_deleted:\n        other: Публікацію видалено.\n      under_review:\n        other: Ваше повідомлення очікує на розгляд. Його буде видно після того, як воно буде схвалено.\n      not_found:\n        other: Питання не знайдено.\n      cannot_deleted:\n        other: Немає дозволу на видалення.\n      cannot_close:\n        other: Немає дозволу на закриття.\n      cannot_update:\n        other: Немає дозволу на оновлення.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Ранг репутації не відповідає умові.\n      vote_fail_to_meet_the_condition:\n        other: Дякуємо за відгук. Щоб проголосувати, вам потрібна репутація не нижче {{.Rank}}.\n      no_enough_rank_to_operate:\n        other: Щоб це зробити, вам потрібна репутація не менше {{.Rank}}.\n    report:\n      handle_failed:\n        other: Не вдалося обробити звіт.\n      not_found:\n        other: Звіт не знайдено.\n    tag:\n      already_exist:\n        other: Теґ уже існує.\n      not_found:\n        other: Теґ не знайдено.\n      recommend_tag_not_found:\n        other: Рекомендований теґ не існує.\n      recommend_tag_enter:\n        other: Будь ласка, введіть принаймні один необхідний тег.\n      not_contain_synonym_tags:\n        other: Не повинно містити теґи синонімів.\n      cannot_update:\n        other: Немає дозволу на оновлення.\n      is_used_cannot_delete:\n        other: Ви не можете видалити теґ, який використовується.\n      cannot_set_synonym_as_itself:\n        other: Ви не можете встановити синонім поточного тегу як сам тег.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Ім’я відправника не може бути електронною адресою.\n    theme:\n      not_found:\n        other: Тему не знайдено.\n    revision:\n      review_underway:\n        other: Наразі неможливо редагувати, є версія в черзі перегляду.\n      no_permission:\n        other: Немає дозволу на перегляд.\n    user:\n      external_login_missing_user_id:\n        other: Платформа сторонніх розробників не надає унікальний ідентифікатор користувача, тому ви не можете увійти, будь ласка, зв’яжіться з адміністратором вебсайту.\n      external_login_unbinding_forbidden:\n        other: Будь ласка, встановіть пароль для входу до свого облікового запису, перш ніж видалити ім'я користувача.\n      email_or_password_wrong:\n        other:\n          other: Електронна пошта та пароль не збігаються.\n      not_found:\n        other: Користувач не знайдений.\n      suspended:\n        other: Користувач був призупинений.\n      username_invalid:\n        other: Ім'я користувача недійсне.\n      username_duplicate:\n        other: Це ім'я користувача вже використовується.\n      set_avatar:\n        other: Не вдалося встановити аватар.\n      cannot_update_your_role:\n        other: Ви не можете змінити вашу роль.\n      not_allowed_registration:\n        other: На цей час сайт не відкритий для реєстрації.\n      not_allowed_login_via_password:\n        other: Наразі на сайті заборонено вхід за допомогою пароля.\n      access_denied:\n        other: Доступ заборонено\n      page_access_denied:\n        other: Ви не маєте доступу до цієї сторінки.\n      add_bulk_users_format_error:\n        other: \"Помилка формату {{.Field}} біля '{{.Content}}' у рядку {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Кількість користувачів, яких ви додаєте одночасно, має бути в діапазоні 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Не вдалося прочитати конфігурацію\n    database:\n      connection_failed:\n        other: Не вдалося встановити з'єднання з базою даних\n      create_table_failed:\n        other: Не вдалося створити таблицю\n    install:\n      create_config_failed:\n        other: Не вдалося створити config.yaml файл.\n    upload:\n      unsupported_file_format:\n        other: Непідтримуваний формат файлу.\n    site_info:\n      config_not_found:\n        other: Конфігурацію сайту не знайдено.\n    badge:\n      object_not_found:\n        other: Об'єкт значка не знайдено\n  reason:\n    spam:\n      name:\n        other: спам\n      desc:\n        other: Це повідомлення є рекламою або вандалізмом. Воно не є корисним або не має відношення до поточної теми.\n    rude_or_abusive:\n      name:\n        other: грубо чи образливо\n      desc:\n        other: \"Розумна людина вважатиме такий зміст неприйнятним для ввічливого спілкування.\"\n    a_duplicate:\n      name:\n        other: дублікат\n      desc:\n        other: Це питання ставилося раніше, і на нього вже є відповідь.\n      placeholder:\n        other: Введіть наявне посилання на питання\n    not_a_answer:\n      name:\n        other: не відповідь\n      desc:\n        other: \"Це повідомлення було опубліковане як відповідь, але воно не є спробою відповісти на запитання. Можливо, його слід відредагувати, прокоментувати, поставити інше запитання або взагалі видалити.\"\n    no_longer_needed:\n      name:\n        other: більше не потрібно\n      desc:\n        other: Цей коментар є застарілим, розмовним або не стосується цієї публікації.\n    something:\n      name:\n        other: інше\n      desc:\n        other: Ця публікація вимагає уваги персоналу з іншої причини, що не вказана вище.\n      placeholder:\n        other: Дайте нам знати, що саме вас турбує\n    community_specific:\n      name:\n        other: причина для спільноти\n      desc:\n        other: Це запитання не відповідає правилам спільноти.\n    not_clarity:\n      name:\n        other: потребує деталей або ясності\n      desc:\n        other: Наразі це запитання містить кілька запитань в одному. Воно має бути зосереджене лише на одній проблемі.\n    looks_ok:\n      name:\n        other: виглядає добре\n      desc:\n        other: Цей допис хороший, як є, і не є низької якості.\n    needs_edit:\n      name:\n        other: потребує редагування, і я це зробив\n      desc:\n        other: Поліпшіть та виправте проблеми з цією публікацією самостійно.\n    needs_close:\n      name:\n        other: потрібно закрити\n      desc:\n        other: Закрите питання не може відповісти, але все ще може редагувати, голосувати і коментувати.\n    needs_delete:\n      name:\n        other: потрібно видалити\n      desc:\n        other: Цей допис буде видалено.\n  question:\n    close:\n      duplicate:\n        name:\n          other: спам\n        desc:\n          other: Це питання ставилося раніше, і на нього вже є відповідь.\n      guideline:\n        name:\n          other: причина для спільноти\n        desc:\n          other: Це запитання не відповідає правилам спільноти.\n      multiple:\n        name:\n          other: потребує деталей або ясності\n        desc:\n          other: Наразі це питання включає кілька запитань в одному. Воно має зосереджуватися лише на одній проблемі.\n      other:\n        name:\n          other: інше\n        desc:\n          other: Для цього допису потрібна інша причина, не зазначена вище.\n    operation_type:\n      asked:\n        other: запитав\n      answered:\n        other: відповів\n      modified:\n        other: змінено\n    deleted_title:\n      other: Видалене питання\n    questions_title:\n      other: Питання\n  tag:\n    tags_title:\n      other: Теґи\n    no_description:\n      other: Тег не має опису.\n  notification:\n    action:\n      update_question:\n        other: оновлене питання\n      answer_the_question:\n        other: питання з відповіддю\n      update_answer:\n        other: оновлена відповідь\n      accept_answer:\n        other: прийнята відповідь\n      comment_question:\n        other: прокоментоване питання\n      comment_answer:\n        other: прокоментована відповідь\n      reply_to_you:\n        other: відповів(-ла) вам\n      mention_you:\n        other: згадав(-ла) вас\n      your_question_is_closed:\n        other: Ваше запитання закрито\n      your_question_was_deleted:\n        other: Ваше запитання видалено\n      your_answer_was_deleted:\n        other: Вашу відповідь видалено\n      your_comment_was_deleted:\n        other: Ваш коментар видалено\n      up_voted_question:\n        other: питання, за яке найбільше проголосували\n      down_voted_question:\n        other: питання, за яке проголосували менше\n      up_voted_answer:\n        other: відповідь, за яку проголосували найбільше\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: коментар, за який проголосували\n      invited_you_to_answer:\n        other: запросив(-ла) вас відповісти\n      earned_badge:\n        other: Ви заробили бейдж \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Підтвердіть нову адресу електронної пошти\"\n      body:\n        other: \"Підтвердьте свою нову адресу електронної пошти для {{.SiteName}} натиснувши на наступне посилання:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nЯкщо ви не запитували цю зміну, будь ласка, ігноруйте цей лист.<br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} відповів(-ла) на ваше запитання\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Переглянути на {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Відписатися</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} запросив(-ла) вас відповісти\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Думаю, ви можете знати відповідь.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Переглянути на {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Відписатися</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} прокоментували ваш допис\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Переглянути на {{.SiteName}}</a><br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Відписатися</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Нове питання: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Скидання пароля\"\n      body:\n        other: \"Хтось попросив скинути ваш пароль на {{.SiteName}}.<br><br>\\n\\nЯкщо це не ви, можете сміливо ігнорувати цей лист.<br><br>\\n\\nПерейдіть за наступним посиланням, щоб вибрати новий пароль:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Підтвердьте свій новий обліковий запис\"\n      body:\n        other: \"Ласкаво просимо до {{.SiteName}}!<br><br>\\n\\nПерейдіть за наступним посиланням, щоб підтвердити та активувати свій новий обліковий запис:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nЯкщо наведене вище посилання не відкривається, спробуйте скопіювати і вставити його в адресний рядок вашого веб-браузера.\\n<br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Тестовий електронний лист\"\n      body:\n        other: \"Це тестовий електронний лист.\\n<br><br>\\n\\n--<br>\\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.\"\n  action_activity_type:\n    upvote:\n      other: підтримати\n    upvoted:\n      other: підтримано\n    downvote:\n      other: голос \"проти\"\n    downvoted:\n      other: проголосував проти\n    accept:\n      other: прийняти\n    accepted:\n      other: прийнято\n    edit:\n      other: редагувати\n  review:\n    queued_post:\n      other: Допис у черзі\n    flagged_post:\n      other: Відмічений пост\n    suggested_post_edit:\n      other: Запропоновані зміни\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} і {{ .Count }} більше...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Автобіограф\n        desc:\n          other: Заповнена <a href=\"{{ .ProfileURL }}\" target=\"_blank\">інформація про профіль</a>.\n      certified:\n        name:\n          other: Підтверджений\n        desc:\n          other: Завершено наш новий посібник користувача.\n      editor:\n        name:\n          other: Редактор\n        desc:\n          other: Перше редагування посту.\n      first_flag:\n        name:\n          other: Перший прапор\n        desc:\n          other: Спочатку позначено допис.\n      first_upvote:\n        name:\n          other: Перший голос за\n        desc:\n          other: Першим голосував за допис.\n      first_link:\n        name:\n          other: Перше посилання\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: Перша реакція\n        desc:\n          other: Першим відреагував на допис.\n      first_share:\n        name:\n          other: Перше поширення\n        desc:\n          other: Перший поділився публікацією.\n      scholar:\n        name:\n          other: Вчений\n        desc:\n          other: Поставив питання і прийняв відповідь.\n      commentator:\n        name:\n          other: Коментатор\n        desc:\n          other: Залиште 5 коментарів.\n      new_user_of_the_month:\n        name:\n          other: Новий користувач місяця\n        desc:\n          other: Видатні внески за їх перший місяць.\n      read_guidelines:\n        name:\n          other: Прочитайте Інструкцію\n        desc:\n          other: Прочитайте [рекомендації для спільноти].\n      reader:\n        name:\n          other: Читач\n        desc:\n          other: Прочитайте кожну відповідь у темі з більш ніж 10 відповідями.\n      welcome:\n        name:\n          other: Ласкаво просимо\n        desc:\n          other: Отримав голос.\n      nice_share:\n        name:\n          other: Гарне поширення\n        desc:\n          other: Поділилися постом з 25 унікальними відвідувачами.\n      good_share:\n        name:\n          other: Хороше поширення\n        desc:\n          other: Поділилися постом з 300 унікальними відвідувачами.\n      great_share:\n        name:\n          other: Відмінне поширення\n        desc:\n          other: Поділилися постом з 1000 унікальними відвідувачами.\n      out_of_love:\n        name:\n          other: З любові\n        desc:\n          other: Використав 50 голосів «за» за день.\n      higher_love:\n        name:\n          other: Вище кохання\n        desc:\n          other: Використав 50 голосів «за» за день 5 разів.\n      crazy_in_love:\n        name:\n          other: Божевільний в любові\n        desc:\n          other: Використав 50 голосів «за» за день 20 разів.\n      promoter:\n        name:\n          other: Промоутер\n        desc:\n          other: Запросив користувача.\n      campaigner:\n        name:\n          other: Агітатор\n        desc:\n          other: Запрошено 3 основних користувачів.\n      champion:\n        name:\n          other: Чемпіон\n        desc:\n          other: Запросив 5 учасників.\n      thank_you:\n        name:\n          other: Дякую\n        desc:\n          other: Має 20 дописів, за які проголосували, і віддав 10 голосів «за».\n      gives_back:\n        name:\n          other: Дає назад\n        desc:\n          other: Має 100 дописів, за які проголосували, і віддав 100 голосів «за».\n      empathetic:\n        name:\n          other: Емпатичний\n        desc:\n          other: Має 500 дописів, за які проголосували, і віддав 1000 голосів «за».\n      enthusiast:\n        name:\n          other: Ентузіаст\n        desc:\n          other: Відвідано 10 днів поспіль.\n      aficionado:\n        name:\n          other: Шанувальник\n        desc:\n          other: Відвідано 100 днів поспіль.\n      devotee:\n        name:\n          other: Відданий\n        desc:\n          other: Відвідано 365 днів поспіль.\n      anniversary:\n        name:\n          other: Річниця\n        desc:\n          other: Активний учасник на рік, опублікував принаймні один раз.\n      appreciated:\n        name:\n          other: Оцінений\n        desc:\n          other: Отримано 1 голос за 20 дописів.\n      respected:\n        name:\n          other: Шанований\n        desc:\n          other: Отримано 2 голоси за 100 дописів.\n      admired:\n        name:\n          other: Захоплений\n        desc:\n          other: Отримано 5 голосів за 300 дописів.\n      solved:\n        name:\n          other: Вирішено\n        desc:\n          other: Нехай відповідь буде прийнята.\n      guidance_counsellor:\n        name:\n          other: Радник супроводу\n        desc:\n          other: Прийміть 10 відповідей.\n      know_it_all:\n        name:\n          other: Усезнайко\n        desc:\n          other: Було прийнято 50 відповідей.\n      solution_institution:\n        name:\n          other: Інституція рішення\n        desc:\n          other: Було прийнято 150 відповідей.\n      nice_answer:\n        name:\n          other: Чудова відповідь\n        desc:\n          other: Оцінка відповіді на 10 або більше.\n      good_answer:\n        name:\n          other: Гарна відповідь\n        desc:\n          other: Оцінка відповіді на 25 або більше.\n      great_answer:\n        name:\n          other: Чудова відповідь\n        desc:\n          other: Оцінка відповіді на 50 або більше.\n      nice_question:\n        name:\n          other: Гарне питання\n        desc:\n          other: Оцінка питання на 10 або більше.\n      good_question:\n        name:\n          other: Хороше питання\n        desc:\n          other: Оцінка питання на 25 або більше.\n      great_question:\n        name:\n          other: Відмінне питання\n        desc:\n          other: Оцінка питання на 50 або більше.\n      popular_question:\n        name:\n          other: Популярне питання\n        desc:\n          other: Питання з 500 переглядами.\n      notable_question:\n        name:\n          other: Помітне питання\n        desc:\n          other: Питання з 1000 переглядами.\n      famous_question:\n        name:\n          other: Знамените питання\n        desc:\n          other: Питання з 5000 переглядами.\n      popular_link:\n        name:\n          other: Популярне посилання\n        desc:\n          other: Опубліковано зовнішнє посилання з 50 натисканнями.\n      hot_link:\n        name:\n          other: Гаряче посилання\n        desc:\n          other: Опубліковано зовнішнє посилання з 300 натисканнями.\n      famous_link:\n        name:\n          other: Знамените Посилання\n        desc:\n          other: Опубліковано зовнішнє посилання зі 100 натисканнями.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Початок роботи\n      community:\n        name:\n          other: Спільнота\n      posting:\n        name:\n          other: Публікація\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Як відформатувати\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: Назад\n    next: Далі\n  page_title:\n    question: Запитання\n    questions: Запитання\n    tag: Теґ\n    tags: Теґи\n    tag_wiki: тег вікі\n    create_tag: Створити теґ\n    edit_tag: Редагувати теґ\n    ask_a_question: Create Question\n    edit_question: Редагувати запитання\n    edit_answer: Редагувати відповідь\n    search: Пошук\n    posts_containing: Публікації, що містять\n    settings: Налаштування\n    notifications: Сповіщення\n    login: Увійти\n    sign_up: Зареєструватися\n    account_recovery: Відновлення облікового запису\n    account_activation: Активація облікового запису\n    confirm_email: Підтвердити електронну адресу\n    account_suspended: Обліковий запис призупинено\n    admin: Адмін\n    change_email: Змінити електронну адресу\n    install: Встановлення Answer\n    upgrade: Оновлення Answer\n    maintenance: Технічне обслуговування сайту\n    users: Користувачі\n    oauth_callback: Обробка\n    http_404: Помилка HTTP 404\n    http_50X: Помилка HTTP 500\n    http_403: Помилка HTTP 403\n    logout: Вийти\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Сповіщення\n    inbox: Вхідні\n    achievement: Досягнення\n    new_alerts: Нові сповіщення\n    all_read: Позначити все як прочитане\n    show_more: Показати більше\n    someone: Хтось\n    inbox_type:\n      all: Усі\n      posts: Публікації\n      invites: Запрошення\n      votes: Голоси\n    answer: Відповідь\n    question: Запитання\n    badge_award: Значок\n  suspended:\n    title: Ваш обліковий запис було призупинено\n    until_time: \"Ваш обліковий запис призупинено до {{ time }}.\"\n    forever: Цього користувача призупинено назавжди.\n    end: Ви не дотримуєтеся правил спільноти.\n    contact_us: Зв'яжіться з нами\n  editor:\n    blockquote:\n      text: Блок Цитування\n    bold:\n      text: Надійний\n    chart:\n      text: Діаграма\n      flow_chart: Блок-схема\n      sequence_diagram: Діаграма послідовності\n      class_diagram: Діаграма класів\n      state_diagram: Діаграма станів\n      entity_relationship_diagram: Діаграма зв'язків сутностей\n      user_defined_diagram: Визначена користувачем діаграма\n      gantt_chart: Діаграма Ґанта\n      pie_chart: Кругова діаграма\n    code:\n      text: Зразок коду\n      add_code: Додати зразок коду\n      form:\n        fields:\n          code:\n            label: Код\n            msg:\n              empty: Код не може бути порожнім.\n          language:\n            label: Мова\n            placeholder: Автоматичне визначення\n      btn_cancel: Скасувати\n      btn_confirm: Додати\n    formula:\n      text: Формула\n      options:\n        inline: Вбудована формула\n        block: Формула блоку\n    heading:\n      text: Заголовок\n      options:\n        h1: Заголовок 1\n        h2: Заголовок 2\n        h3: Заголовок 3\n        h4: Заголовок 4\n        h5: Заголовок 5\n        h6: Заголовок 6\n    help:\n      text: Допомога\n    hr:\n      text: Горизонтальна лінійка\n    image:\n      text: Зображення\n      add_image: Додати зображення\n      tab_image: Завантажити зображення\n      form_image:\n        fields:\n          file:\n            label: Файл зображення\n            btn: Обрати зображення\n            msg:\n              empty: Файл не може бути порожнім.\n              only_image: Допустимі лише файли зображень.\n              max_size: Розмір файлу не може перевищувати {{size}} МБ.\n          desc:\n            label: Опис\n      tab_url: URL зображення\n      form_url:\n        fields:\n          url:\n            label: URL зображення\n            msg:\n              empty: URL-адреса зображення не може бути пустою.\n          name:\n            label: Опис\n      btn_cancel: Скасувати\n      btn_confirm: Додати\n      uploading: Завантаження\n    indent:\n      text: Абзац\n    outdent:\n      text: Відступ\n    italic:\n      text: Акцент\n    link:\n      text: Гіперпосилання\n      add_link: Додати гіперпосилання\n      form:\n        fields:\n          url:\n            label: URL\n            msg:\n              empty: URL-адреса не може бути пустою.\n          name:\n            label: Опис\n      btn_cancel: Скасувати\n      btn_confirm: Додати\n    ordered_list:\n      text: Нумерований список\n    unordered_list:\n      text: Маркований список\n    table:\n      text: Таблиця\n      heading: Заголовок\n      cell: Клітинка\n    file:\n      text: Прикріпити файли\n      not_supported: \"Не підтримується цей тип файлу. Спробуйте ще раз з {{file_type}}.\"\n      max_size: \"Розмір прикріплених файлів не може перевищувати {{size}} МБ.\"\n  close_modal:\n    title: Я закриваю цей пост, оскільки...\n    btn_cancel: Скасувати\n    btn_submit: Надіслати\n    remark:\n      empty: Не може бути порожнім.\n    msg:\n      empty: Будь ласка, оберіть причину.\n  report_modal:\n    flag_title: Я ставлю відмітку, щоб повідомити про цю публікацію як...\n    close_title: Я закриваю цей пост, оскільки...\n    review_question_title: Переглянути питання\n    review_answer_title: Переглянути відповідь\n    review_comment_title: Переглянути коментар\n    btn_cancel: Скасувати\n    btn_submit: Надіслати\n    remark:\n      empty: Не може бути порожнім.\n    msg:\n      empty: Будь ласка, оберіть причину.\n      not_a_url: Формат URL неправильний.\n      url_not_match: Походження URL не збігається з поточним вебсайтом.\n  tag_modal:\n    title: Створити новий теґ\n    form:\n      fields:\n        display_name:\n          label: Ім'я для відображення\n          msg:\n            empty: Ім'я для відображення не може бути порожнім.\n            range: Ім'я для відображення до 35 символів.\n        slug_name:\n          label: Скорочена URL-адреса\n          desc: Скорочення URL до 35 символів.\n          msg:\n            empty: Скорочення URL не може бути пустим.\n            range: Скорочення URL до 35 символів.\n            character: Скорочення URL містить незадовільний набір символів.\n        desc:\n          label: Опис\n        revision:\n          label: Редакція\n        edit_summary:\n          label: Підсумок редагування\n          placeholder: >-\n            Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування)\n    btn_cancel: Скасувати\n    btn_submit: Надіслати\n    btn_post: Опублікувати новий теґ\n  tag_info:\n    created_at: Створено\n    edited_at: Відредаговано\n    history: Історія\n    synonyms:\n      title: Синоніми\n      text: Наступні теги буде змінено на\n      empty: Синонімів не знайдено.\n      btn_add: Додати синонім\n      btn_edit: Редагувати\n      btn_save: Зберегти\n    synonyms_text: Наступні теги буде змінено на\n    delete:\n      title: Видалити цей теґ\n      tip_with_posts: >-\n        <p>Ми не дозволяємо <strong>видаляти тег з дописами</strong>.</p> <p>Передусім, будь ласка, вилучіть цей тег з дописів.</p>\n      tip_with_synonyms: >-\n        <p>Ми не дозволяємо <strong>видаляти тег із синонімами</strong>.</p> <p>Передусім, будь ласка, вилучіть синоніми з цього тега.</p>\n      tip: Ви впевнені, що хочете видалити?\n      close: Закрити\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Редагувати теґ\n    default_reason: Редагувати теґ\n    default_first_reason: Додати теґ\n    btn_save_edits: Зберегти зміни\n    btn_cancel: Скасувати\n  dates:\n    long_date: МММ Д\n    long_date_with_year: \"МММ Д, РРРР\"\n    long_date_with_time: \"МММ Д, РРРР [о] ГГ:хв\"\n    now: зараз\n    x_seconds_ago: \"{{count}}сек назад\"\n    x_minutes_ago: \"{{count}}хв назад\"\n    x_hours_ago: \"{{count}}год назад\"\n    hour: година\n    day: день\n    hours: годин\n    days: дні\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: серце\n    smile: посмішка\n    frown: насупився\n    btn_label: додавати або вилучати реакції\n    undo_emoji: скасувати реакцію {{ emoji }}\n    react_emoji: реагувати з {{ emoji }}\n    unreact_emoji: не реагувати з {{ emoji }}\n  comment:\n    btn_add_comment: Додати коментар\n    reply_to: Відповісти на\n    btn_reply: Відповісти\n    btn_edit: Редагувати\n    btn_delete: Видалити\n    btn_flag: Відмітити\n    btn_save_edits: Зберегти зміни\n    btn_cancel: Скасувати\n    show_more: \"Ще {{count}} коментарів\"\n    tip_question: >-\n      Використовуйте коментарі, щоб попросити більше інформації або запропонувати покращення. Уникайте відповідей на питання в коментарях.\n    tip_answer: >-\n      Використовуйте коментарі, щоб відповідати іншим користувачам або повідомляти їх про зміни. Якщо ви додаєте нову інформацію, відредагуйте свою публікацію, а не коментуйте.\n    tip_vote: Це додає щось корисне до допису\n  edit_answer:\n    title: Редагувати відповідь\n    default_reason: Редагувати відповідь\n    default_first_reason: Додати відповідь\n    form:\n      fields:\n        revision:\n          label: Редакція\n        answer:\n          label: Відповідь\n          feedback:\n            characters: вміст має бути не менше 6 символів.\n        edit_summary:\n          label: Редагувати підсумок\n          placeholder: >-\n            Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування)\n    btn_save_edits: Зберегти зміни\n    btn_cancel: Скасувати\n  tags:\n    title: Теґи\n    sort_buttons:\n      popular: Популярне\n      name: Назва\n      newest: Найновіші\n    button_follow: Підписатися\n    button_following: Підписані\n    tag_label: запитання\n    search_placeholder: Фільтрувати за назвою теґу\n    no_desc: Цей теґ не має опису.\n    more: Більше\n    wiki: Вікі\n  ask:\n    title: Create Question\n    edit_title: Редагувати питання\n    default_reason: Редагувати питання\n    default_first_reason: Create question\n    similar_questions: Подібні питання\n    form:\n      fields:\n        revision:\n          label: Редакція\n        title:\n          label: Назва\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Назва не може бути порожньою.\n            range: Назва до 150 символів\n        body:\n          label: Тіло\n          msg:\n            empty: Тіло не може бути порожнім.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Теґи\n          msg:\n            empty: Теґи не можуть бути порожніми.\n        answer:\n          label: Відповідь\n          msg:\n            empty: Відповідь не може бути порожньою.\n        edit_summary:\n          label: Редагувати підсумок\n          placeholder: >-\n            Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування)\n    btn_post_question: Опублікуйте своє запитання\n    btn_save_edits: Зберегти зміни\n    answer_question: Відповісти на власне питання\n    post_question&answer: Опублікуйте своє запитання і відповідь\n  tag_selector:\n    add_btn: Додати теґ\n    create_btn: Створити новий теґ\n    search_tag: Шукати теґ\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Не знайдено тегів\n    tag_required_text: Обов'язковий тег (принаймні один)\n  header:\n    nav:\n      question: Запитання\n      tag: Теґи\n      user: Користувачі\n      badges: Значки\n      profile: Профіль\n      setting: Налаштування\n      logout: Вийти\n      admin: Адмін\n      review: Огляд\n      bookmark: Закладки\n      moderation: Модерація\n    search:\n      placeholder: Пошук\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Змінити\n    loading: завантаження...\n  pic_auth_code:\n    title: Капча\n    placeholder: Введіть текст вище\n    msg:\n      empty: Капча не може бути порожньою.\n  inactive:\n    first: >-\n      Ви майже закінчили! Ми надіслали лист для активації на <bold>{{mail}}</bold>. Будь ласка, дотримуйтесь інструкцій, щоб активувати свій обліковий запис.\n    info: \"Якщо він не надійшов, перевірте папку зі спамом.\"\n    another: >-\n      Ми надіслали вам інший електронний лист для активації на <bold>{{mail}}</bold>. Це може зайняти кілька хвилин, перш ніж він прибуде; обов'язково перевірте теку зі спамом.\n    btn_name: Повторно надіслати електронний лист для активації\n    change_btn_name: Змінити електронну пошту\n    msg:\n      empty: Не може бути порожнім.\n    resend_email:\n      url_label: Ви впевнені, що бажаєте повторно надіслати електронний лист для активації?\n      url_text: Ви також можете дати користувачеві наведене вище посилання для активації.\n  login:\n    login_to_continue: Увійдіть, щоб продовжити\n    info_sign: Немає облікового запису? <1>Зареєструйтесь</1>\n    info_login: Вже маєте обліковий запис? <1>Увійдіть</1>\n    agreements: Реєструючись, ви погоджуєтеся з <1>політикою конфіденційності</1> та <3>умовами використання</3>.\n    forgot_pass: Забули пароль?\n    name:\n      label: Ім’я\n      msg:\n        empty: Ім'я не може бути порожнім.\n        range: Ім'я повинно мати довжину від 2 до 30 символів.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Електронна пошта\n      msg:\n        empty: Поле електронної пошти не може бути пустим.\n    password:\n      label: Пароль\n      msg:\n        empty: Поле паролю не може бути порожнім.\n        different: Двічі введені паролі є несумісними\n  account_forgot:\n    page_title: Забули свій пароль\n    btn_name: Надішліть мені електронний лист для відновлення\n    send_success: >-\n      Якщо обліковий запис збігається з <strong>{{mail}}</strong>, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля.\n    email:\n      label: Електронна пошта\n      msg:\n        empty: Поле електронної пошти не може бути пустим.\n  change_email:\n    btn_cancel: Скасувати\n    btn_update: Оновити адресу електронної пошти\n    send_success: >-\n      Якщо обліковий запис збігається з <strong>{{mail}}</strong>, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля.\n    email:\n      label: Нова електронна пошта\n      msg:\n        empty: Поле електронної пошти не може бути пустим.\n  oauth:\n    connect: З'єднати з {{ auth_name }}\n    remove: Видалити {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Додайте резервну електронну пошту до свого облікового запису.\n    btn_update: Оновити адресу електронної пошти\n    email:\n      label: Електронна пошта\n      msg:\n        empty: Поле електронної пошти не може бути пустим.\n    modal_title: Електронна адреса вже існує.\n    modal_content: Ця електронна адреса вже зареєстрована. Ви впевнені, що бажаєте підключитися до існуючого облікового запису?\n    modal_cancel: Змінити електронну пошту\n    modal_confirm: Під'єднати до існуючого облікового запису\n  password_reset:\n    page_title: Скинути пароль\n    btn_name: Скинути мій пароль\n    reset_success: >-\n      Ви успішно змінили пароль; вас буде перенаправлено на сторінку входу в систему.\n    link_invalid: >-\n      На жаль, це посилання для зміни пароля більше недійсне. Можливо, ваш пароль уже скинуто?\n    to_login: Продовжити вхід на сторінку\n    password:\n      label: Пароль\n      msg:\n        empty: Поле паролю не може бути порожнім.\n        length: Довжина повинна бути від 8 до 32 символів\n        different: Двічі введені паролі є несумісними\n    password_confirm:\n      label: Підтвердити новий пароль\n  settings:\n    page_title: Налаштування\n    goto_modify: Перейти до зміни\n    nav:\n      profile: Профіль\n      notification: Сповіщення\n      account: Обліковий запис\n      interface: Інтерфейс\n    profile:\n      heading: Профіль\n      btn_name: Зберегти\n      display_name:\n        label: Ім'я для відображення\n        msg: Ім'я для відображення не може бути порожнім.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Ім'я користувача\n        caption: Користувачі можуть згадувати вас як \"@username\".\n        msg: Ім’я користувача не може бути порожнім.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Зображення профілю\n        gravatar: Gravatar\n        gravatar_text: Ви можете змінити зображення на\n        custom: Власне\n        custom_text: Ви можете завантажити своє зображення.\n        default: Системне\n        msg: Будь ласка, завантажте аватар\n      bio:\n        label: Про мене\n      website:\n        label: Вебсайт\n        placeholder: \"https://example.com\"\n        msg: Неправильний формат вебсайту\n      location:\n        label: Місцезнаходження\n        placeholder: \"Місто, Країна\"\n    notification:\n      heading: Сповіщення електронною поштою\n      turn_on: Увімкнути\n      inbox:\n        label: Вхідні сповіщення\n        description: Відповіді на ваші запитання, коментарі, запрошення тощо.\n      all_new_question:\n        label: Усі нові запитання\n        description: Отримуйте сповіщення про всі нові питання. До 50 питань на тиждень.\n      all_new_question_for_following_tags:\n        label: Всі нові запитання з наступними тегами\n        description: Отримувати сповіщення про нові запитання з наступними тегами.\n    account:\n      heading: Обліковий запис\n      change_email_btn: Змінити електронну пошту\n      change_pass_btn: Змінити пароль\n      change_email_info: >-\n        Ми надіслали електронний лист на цю адресу. Будь ласка, дотримуйтесь інструкцій для підтвердження.\n      email:\n        label: Нова електронна пошта\n      new_email:\n        label: Нова електронна пошта\n        msg: Нова електронна пошта не може бути порожньою.\n      pass:\n        label: Поточний пароль\n        msg: Комірка паролю не може бути порожньою.\n      password_title: Пароль\n      current_pass:\n        label: Поточний пароль\n        msg:\n          empty: Комірка поточного пароля не може бути порожньою.\n          length: Довжина повинна бути від 8 до 32 символів.\n          different: Два введені паролі не збігаються.\n      new_pass:\n        label: Новий пароль\n      pass_confirm:\n        label: Підтвердити новий пароль\n    interface:\n      heading: Інтерфейс\n      lang:\n        label: Мова інтерфейсу\n        text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку.\n    my_logins:\n      title: Мої логіни\n      label: Увійдіть або зареєструйтеся на цьому сайті, використовуючи ці облікові записи.\n      modal_title: Видалити логін\n      modal_content: Ви впевнені, що хочете видалити цей логін з облікового запису?\n      modal_confirm_btn: Видалити\n      remove_success: Успішно видалено\n  toast:\n    update: успішно оновлено\n    update_password: Пароль успішно змінено.\n    flag_success: Дякую, що відмітили.\n    forbidden_operate_self: Заборонено застосовувати на собі\n    review: Ваша версія з'явиться після перевірки.\n    sent_success: Успішно відправлено\n  related_question:\n    title: Related\n    answers: відповіді\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Люди запитували\n    desc: Виберіть людей, які, на вашу думку, можуть знати відповідь.\n    invite: Запросити відповісти\n    add: Додати людей\n    search: Шукати людей\n  question_detail:\n    action: Дія\n    created: Created\n    Asked: Запитали\n    asked: запитали\n    update: Змінено\n    Edited: Edited\n    edit: відредаговано\n    commented: прокоментовано\n    Views: Переглянуто\n    Follow: Підписатися\n    Following: Підписані\n    follow_tip: Підпишіться на це запитання, щоб отримувати сповіщення\n    answered: дано відповідь\n    closed_in: Зачинено в\n    show_exist: Показати наявне запитання.\n    useful: Корисне\n    question_useful: Це корисно і ясно\n    question_un_useful: Це неясно або некорисно\n    question_bookmark: Додати в закладки це питання\n    answer_useful: Це корисно\n    answer_un_useful: Це некорисно\n    answers:\n      title: Відповіді\n      score: Оцінка\n      newest: Найновіші\n      oldest: Найдавніші\n      btn_accept: Прийняти\n      btn_accepted: Прийнято\n    write_answer:\n      title: Ваша відповідь\n      edit_answer: Редагувати мою чинну відповідь\n      btn_name: Опублікувати свою відповідь\n      add_another_answer: Додати ще одну відповідь\n      confirm_title: Перейти до відповіді\n      continue: Продовжити\n      confirm_info: >-\n        <p>Ви впевнені, що хочете додати ще одну відповідь?</p><p>Натомість ви можете скористатися посиланням редагування, щоб уточнити та покращити вже існуючу відповідь.</p>\n      empty: Відповідь не може бути порожньою.\n      characters: вміст має бути не менше 6 символів.\n      tips:\n        header_1: Дякуємо за відповідь\n        li1_1: Будь ласка, не забудьте <strong>відповісти на запитання</strong>. Надайте детальну інформацію та поділіться своїми дослідженнями.\n        li1_2: Підкріплюйте будь-які ваші твердження посиланнями чи особистим досвідом.\n        header_2: Але <strong>уникайте</strong>...\n        li2_1: Просити про допомогу, шукати роз'яснення або реагувати на інші відповіді.\n    reopen:\n      confirm_btn: Відкрити знову\n      title: Повторно відкрити цей допис\n      content: Ви впевнені, що хочете повторно відкрити?\n    list:\n      confirm_btn: Список\n      title: Показати цей допис\n      content: Ви впевнені, що хочете скласти список?\n    unlist:\n      confirm_btn: Вилучити зі списку\n      title: Вилучити допис зі списку\n      content: Ви впевнені, що хочете вилучити зі списку?\n    pin:\n      title: Закріпити цей допис\n      content: Ви впевнені, що хочете закріпити глобально? Цей допис відображатиметься вгорі всіх списків публікацій.\n      confirm_btn: Закріпити\n  delete:\n    title: Видалити цей допис\n    question: >-\n      Ми не рекомендуємо <strong>видаляти питання з відповідями</strong>, оскільки це позбавляє майбутніх читачів цих знань.</p><p>Повторне видалення запитань із відповідями може призвести до блокування запитів у вашому обліковому записі. Ви впевнені, що хочете видалити?\n    answer_accepted: >-\n      <p>Ми не рекомендуємо <strong>видаляти прийняту відповідь</strong>, оскільки це позбавляє майбутніх читачів цих знань. </p> Повторне видалення прийнятих відповідей може призвести до того, що ваш обліковий запис буде заблоковано для відповідей. Ви впевнені, що хочете видалити?\n    other: Ви впевнені, що хочете видалити?\n    tip_answer_deleted: Ця відповідь була видалена\n    undelete_title: Скасувати видалення цього допису\n    undelete_desc: Ви впевнені, що бажаєте скасувати видалення?\n  btns:\n    confirm: Підтвердити\n    cancel: Скасувати\n    edit: Редагувати\n    save: Зберегти\n    delete: Видалити\n    undelete: Скасувати видалення\n    list: Список\n    unlist: Вилучити зі списку\n    unlisted: Вилучене зі списку\n    login: Увійти\n    signup: Зареєструватися\n    logout: Вийти\n    verify: Підтвердити\n    create: Create\n    approve: Затвердити\n    reject: Відхилити\n    skip: Пропустити\n    discard_draft: Видалити чернетку\n    pinned: Закріплено\n    all: Усі\n    question: Запитання\n    answer: Відповідь\n    comment: Коментар\n    refresh: Оновити\n    resend: Надіслати повторно\n    deactivate: Деактивувати\n    active: Активні\n    suspend: Призупинити\n    unsuspend: Відновити\n    close: Закрити\n    reopen: Відкрити знову\n    ok: ОК\n    light: Світла\n    dark: Темна\n    system_setting: Налаштування системи\n    default: За замовчуванням\n    reset: Скинути\n    tag: Тег\n    post_lowercase: допис\n    filter: Фільтр\n    ignore: Ігнорувати\n    submit: Надіслати\n    normal: Нормальний\n    closed: Закриті\n    deleted: Видалені\n    deleted_permanently: Deleted permanently\n    pending: Очікування\n    more: Більше\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Результати пошуку\n    keywords: Ключові слова\n    options: Параметри\n    follow: Підписатися\n    following: Підписані\n    counts: \"{{count}} Результатів\"\n    counts_loading: \"... Results\"\n    more: Більше\n    sort_btns:\n      relevance: Релевантність\n      newest: Найновіші\n      active: Активні\n      score: Оцінка\n      more: Більше\n    tips:\n      title: Підказки щодо розширеного пошуку\n      tag: \"<1>[tag]</1> шукати за тегом\"\n      user: \"<1>користувач:ім'я користувача</1> пошук за автором\"\n      answer: \"<1>відповіді:0</1> питання без відповіді\"\n      score: \"<1>рахунок: 3</1> записи із 3+ рахунком\"\n      question: \"<1>є:питання</1> пошукові питання\"\n      is_answer: \"<1>є:відповідь</1> пошукові відповіді\"\n    empty: Ми не змогли нічого знайти. <br /> Спробуйте різні або менш конкретні ключові слова.\n  share:\n    name: Поділитись\n    copy: Копіювати посилання\n    via: Поділитися дописом через...\n    copied: Скопійовано\n    facebook: Поділитись на Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Ви не можете проголосувати за власну публікацію.\n  modal_confirm:\n    title: Помилка...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Ваш новий обліковий запис підтверджено; вас буде перенаправлено на головну сторінку.\n    link: Перейти на головну сторінку\n    oops: Йой!\n    invalid: Посилання, яке ви використовували, більше не працює.\n    confirm_new_email: Вашу адресу електронної пошти було оновлено.\n    confirm_new_email_invalid: >-\n      На жаль, це посилання для підтвердження більше не дійсне. Можливо, ваша електронна пошта вже була змінена?\n  unsubscribe:\n    page_title: Відписатися\n    success_title: Ви успішно відписалися\n    success_desc: Вас успішно вилучено з цього списку підписників, і ви більше не будете отримувати від нас електронні листи.\n    link: Змінити налаштування\n  question:\n    following_tags: Підписки на теги\n    edit: Редагувати\n    save: Зберегти\n    follow_tag_tip: Підпишіться на теги, щоб упорядкувати свій список запитань.\n    hot_questions: Гарячі питання\n    all_questions: Всі питання\n    x_questions: \"{{ count }} Питань\"\n    x_answers: \"{{ count }} відповідей\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Запитання\n    answers: Відповіді\n    newest: Найновіші\n    active: Активні\n    hot: Гаряче\n    frequent: Часто\n    recommend: Рекомендовано\n    score: Оцінка\n    unanswered: Без відповідей\n    modified: змінено\n    answered: дано відповідь\n    asked: запитано\n    closed: закрито\n    follow_a_tag: Підписатися на тег\n    more: Більше\n  personal:\n    overview: Загальний огляд\n    answers: Відповіді\n    answer: відповідь\n    questions: Запитання\n    question: запитання\n    bookmarks: Закладки\n    reputation: Репутація\n    comments: Коментарі\n    votes: Голоси\n    badges: Значки\n    newest: Найновіше\n    score: Оцінка\n    edit_profile: Редагувати профіль\n    visited_x_days: \"Відвідано {{ count }} днів\"\n    viewed: Переглянуто\n    joined: Приєднано\n    comma: \",\"\n    last_login: Переглянуто\n    about_me: Про мене\n    about_me_empty: \"// Привіт, світ!\"\n    top_answers: Найкращі відповіді\n    top_questions: Найкращі запитання\n    stats: Статистика\n    list_empty: Не знайдено жодного допису.<br />Можливо, ви хочете вибрати іншу вкладку?\n    content_empty: Постів не знайдено.\n    accepted: Прийнято\n    answered: дано відповідь\n    asked: запитано\n    downvoted: проголосовано проти\n    mod_short: MOD\n    mod_long: Модератори\n    x_reputation: репутація\n    x_votes: отримані голоси\n    x_answers: відповіді\n    x_questions: запитання\n    recent_badges: Нещодавні значки\n  install:\n    title: Встановлення\n    next: Далі\n    done: Готово\n    config_yaml_error: Не вдалося створити config.yaml файл.\n    lang:\n      label: Будь ласка, виберіть мову\n    db_type:\n      label: Рушій бази даних\n    db_username:\n      label: Ім'я користувача\n      placeholder: корінь\n      msg: Ім’я користувача не може бути порожнім.\n    db_password:\n      label: Пароль\n      placeholder: корінь\n      msg: Поле паролю не може бути порожнім.\n    db_host:\n      label: Хост бази даних\n      placeholder: \"db:3306\"\n      msg: Хост бази даних не може бути порожнім.\n    db_name:\n      label: Назва бази даних\n      placeholder: відповідь\n      msg: Назва бази даних не може бути порожня.\n    db_file:\n      label: Файл бази даних\n      placeholder: /data/answer.db\n      msg: Файл бази даних не може бути порожнім.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Створити config.yaml\n      label: Файл config.yaml створено.\n      desc: >-\n        Ви можете створити файл <1>config.yaml</1> вручну в каталозі <1>/var/www/xxx/</1> і вставити в нього наступний текст.\n      info: Після цього натисніть кнопку \"Далі\".\n    site_information: Інформація про сайт\n    admin_account: Обліковий запис адміністратора\n    site_name:\n      label: Назва сайту\n      msg: Назва сайту не може бути порожньою.\n      msg_max_length: Назва сайту повинна містити не більше 30 символів.\n    site_url:\n      label: URL сайту\n      text: Адреса вашого сайту.\n      msg:\n        empty: URL-адреса сайту не може бути пустою.\n        incorrect: Неправильний формат URL-адреси сайту.\n        max_length: Максимальна довжина URL-адреси сайту – 512 символів.\n    contact_email:\n      label: Контактна електронна адреса\n      text: Електронна адреса основної контактної особи, відповідальної за цей сайт.\n      msg:\n        empty: Контактна електронна адреса не може бути порожньою.\n        incorrect: Неправильний формат контактної електронної пошти.\n    login_required:\n      label: Приватний\n      switch: Вхід обов'язковий\n      text: Лише авторизовані користувачі можуть отримати доступ до цієї спільноти.\n    admin_name:\n      label: Ім’я\n      msg: Ім'я не може бути порожнім.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Пароль\n      text: >-\n        Вам знадобиться цей пароль для входу. Зберігайте його в надійному місці.\n      msg: Поле паролю не може бути порожнім.\n      msg_min_length: Пароль має бути не менше 8 символів.\n      msg_max_length: Пароль має бути не менше 32 символів.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Електронна пошта\n      text: Вам знадобиться ця електронна адреса для входу.\n      msg:\n        empty: Поле електронної пошти не може бути пустим.\n        incorrect: Невірний формат електронної пошти.\n    ready_title: Ваш сайт готовий\n    ready_desc: >-\n      Якщо ви коли-небудь захочете змінити інші налаштування, відвідайте <1>розділ адміністрування</1>; знайдіть його в меню сайту.\n    good_luck: \"Веселіться, і хай щастить!\"\n    warn_title: Попередження\n    warn_desc: >-\n      Файл <1>config.yaml</1> вже існує. Якщо вам потрібно скинути будь-який з елементів конфігурації в цьому файлі, будь ласка, спочатку видаліть його.\n    install_now: Ви можете спробувати <1>встановити зараз</1>.\n    installed: Уже встановлено\n    installed_desc: >-\n      Ви, здається, уже встановили. Щоб перевстановити, спочатку очистіть старі таблиці бази даних.\n    db_failed: Не вдалося встановити з'єднання з базою даних\n    db_failed_desc: >-\n      Це означає, що інформація про базу даних у вашому файлі <1>config.yaml</1> невірна або що не вдалося встановити контакт із сервером бази даних. Це може означати, що сервер бази даних вашого хоста не працює.\n  counts:\n    views: перегляди\n    votes: голоси\n    answers: відповіді\n    accepted: Схвалено\n  page_error:\n    http_error: Помилка HTTP {{ code }}\n    desc_403: Ви не маєте дозволу на доступ до цієї сторінки.\n    desc_404: На жаль, такої сторінки не існує.\n    desc_50X: Сервер виявив помилку і не зміг виконати ваш запит.\n    back_home: Повернутися на головну сторінку\n  page_maintenance:\n    desc: \"Ми технічно обслуговуємось, ми скоро повернемося.\"\n  nav_menus:\n    dashboard: Панель\n    contents: Зміст\n    questions: Питання\n    answers: Відповіді\n    users: Користувачі\n    badges: Значки\n    flags: Відмітки\n    settings: Налаштування\n    general: Основне\n    interface: Інтерфейс\n    smtp: SMTP\n    branding: Брендинг\n    legal: Правила та умови\n    write: Написати\n    terms: Terms\n    tos: Умови використання\n    privacy: Приватність\n    seo: SEO\n    customize: Персоналізувати\n    themes: Теми\n    login: Вхід\n    privileges: Привілеї\n    plugins: Плагіни\n    installed_plugins: Встановлені плагіни\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Ласкаво просимо до {{site_name}}\n  user_center:\n    login: Вхід\n    qrcode_login_tip: Будь ласка, використовуйте {{ agentName }}, щоб просканувати QR-код і увійти в систему.\n    login_failed_email_tip: Не вдалося увійти, будь ласка, дозвольте цьому додатку отримати доступ до вашої електронної пошти, перш ніж спробувати ще раз.\n  badges:\n    modal:\n      title: Вітаємо\n      content: Ти отримав новий значок.\n      close: Закрити\n      confirm: Переглянути значки\n    title: Значки\n    awarded: Присвоєно\n    earned_×: Зароблено ×{{ number }}\n    ×_awarded: \"Присвоєно {{ number }}\"\n    can_earn_multiple: Ви можете заробити це багато разів.\n    earned: Зароблено\n  admin:\n    admin_header:\n      title: Адмін\n    dashboard:\n      title: Панель\n      welcome: Ласкаво просимо до адміністратора!\n      site_statistics: Статистика сайту\n      questions: \"Запитання:\"\n      resolved: \"Вирішено:\"\n      unanswered: \"Без відповідей:\"\n      answers: \"Відповіді:\"\n      comments: \"Коментарі:\"\n      votes: \"Голоси:\"\n      users: \"Користувачі:\"\n      flags: \"Відмітки:\"\n      reviews: \"Відгуки:\"\n      site_health: Стан сайту\n      version: \"Версія:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Завантажити теку:\"\n      run_mode: \"Активний режим:\"\n      private: Приватний\n      public: Публічний\n      smtp: \"SMTP:\"\n      timezone: \"Часовий пояс:\"\n      system_info: Інформація про систему\n      go_version: \"Перейти до версії:\"\n      database: \"База даних:\"\n      database_size: \"Розмір бази даних:\"\n      storage_used: \"Використаний обсяг пам’яті:\"\n      uptime: \"Час роботи:\"\n      links: Посилання\n      plugins: Плаґіни\n      github: GitHub\n      blog: Блоґ\n      contact: Контакт\n      forum: Форум\n      documents: Документи\n      feedback: Відгук\n      support: Підтримка\n      review: Огляд\n      config: Конфігурація\n      update_to: Оновити до\n      latest: Останній\n      check_failed: Не вдалося перевірити\n      \"yes\": \"Так\"\n      \"no\": \"Ні\"\n      not_allowed: Не дозволено\n      allowed: Дозволено\n      enabled: Увімкнено\n      disabled: Вимкнено\n      writable: Записуваний\n      not_writable: Не можна записувати\n    flags:\n      title: Відмітки\n      pending: В очікуванні\n      completed: Завершено\n      flagged: Відмічено\n      flagged_type: Відмічено {{ type }}\n      created: Створені\n      action: Дія\n      review: Огляд\n    user_role_modal:\n      title: Змінити роль користувача на...\n      btn_cancel: Скасувати\n      btn_submit: Надіслати\n    new_password_modal:\n      title: Встановити новий пароль\n      form:\n        fields:\n          password:\n            label: Пароль\n            text: Користувача буде виведено з системи, і йому потрібно буде увійти знову.\n            msg: Пароль повинен мати довжину від 8 до 32 символів.\n      btn_cancel: Скасувати\n      btn_submit: Надіслати\n    edit_profile_modal:\n      title: Редагувати профіль\n      form:\n        fields:\n          display_name:\n            label: Зображуване ім'я\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Ім'я користувача\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Електронна пошта\n            msg_invalid: Невірна адреса електронної пошти.\n      edit_success: Успішно відредаговано\n      btn_cancel: Скасувати\n      btn_submit: Надіслати\n    user_modal:\n      title: Додати нового користувача\n      form:\n        fields:\n          users:\n            label: Масове додавання користувача\n            placeholder: \"Джон Сміт, john@example.com, BUSYopr2\\nАліса, alice@example.com, fpDntV8q\"\n            text: '“Ім''я, електронну пошту, пароль” розділити комами. Один користувач у рядку.'\n            msg: \"Будь ласка, введіть електронну пошту користувача, по одній на рядок.\"\n          display_name:\n            label: Ім'я для відображення\n            msg: Ім'я для показу повинно мати довжину від 2 до 30 символів.\n          email:\n            label: Електронна пошта\n            msg: Електронна пошта недійсна.\n          password:\n            label: Пароль\n            msg: Пароль повинен мати довжину від 8 до 32 символів.\n      btn_cancel: Скасувати\n      btn_submit: Надіслати\n    users:\n      title: Користувачі\n      name: Ім’я\n      email: Електронна пошта\n      reputation: Репутація\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Статус\n      role: Роль\n      action: Дія\n      change: Зміна\n      all: Усі\n      staff: Персонал\n      more: Більше\n      inactive: Неактивні\n      suspended: Призупинено\n      deleted: Видалено\n      normal: Нормальний\n      Moderator: Модератор\n      Admin: Адмін\n      User: Користувач\n      filter:\n        placeholder: \"Фільтр на ім'я, користувач:id\"\n      set_new_password: Встановити новий пароль\n      edit_profile: Редагувати профіль\n      change_status: Змінити статус\n      change_role: Змінити роль\n      show_logs: Показати записи журналу\n      add_user: Додати користувача\n      deactivate_user:\n        title: Деактивувати користувача\n        content: Неактивний користувач повинен повторно підтвердити свою електронну адресу.\n      delete_user:\n        title: Видалити цього користувача\n        content: Ви впевнені, що хочете видалити цього користувача? Це назавжди!\n        remove: Вилучити їх вміст\n        label: Видалити всі запитання, відповіді, коментарі тощо.\n        text: Не позначайте цю опцію, якщо ви хочете лише видалити обліковий запис користувача.\n      suspend_user:\n        title: Призупинити цього користувача\n        content: Призупинений користувач не може увійти в систему.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Запитання\n      unlisted: Вилучене зі списку\n      post: Опублікувати\n      votes: Голоси\n      answers: Відповіді\n      created: Створені\n      status: Статус\n      action: Дія\n      change: Зміна\n      pending: Очікування\n      filter:\n        placeholder: \"Фільтр за назвою, питання:id\"\n    answers:\n      page_title: Відповіді\n      post: Допис\n      votes: Голоси\n      created: Створено\n      status: Статус\n      action: Дія\n      change: Зміна\n      filter:\n        placeholder: \"Фільтр за назвою, відповідь:id\"\n    general:\n      page_title: Основне\n      name:\n        label: Назва сайту\n        msg: Назва сайту не може бути порожньою.\n        text: \"Назва цього сайту як зазначено у заголовку тегу.\"\n      site_url:\n        label: URL сайту\n        msg: Url сайту не може бути порожньою.\n        validate: Будь ласка, введіть дійсну URL.\n        text: Адреса вашого сайту.\n      short_desc:\n        label: Короткий опис сайту\n        msg: Короткий опис сайту не може бути пустим.\n        text: \"Короткий опис, як використовується в заголовку на головній сторінці.\"\n      desc:\n        label: Опис сайту\n        msg: Опис сайту не може бути порожнім.\n        text: \"Опишіть цей сайт одним реченням, як у тезі метаопису.\"\n      contact_email:\n        label: Контактна електронна пошта\n        msg: Контактна електронна пошта не може бути порожньою.\n        validate: Контактна електронна пошта недійсна.\n        text: Адреса електронної пошти ключової особи, відповідальної за цей сайт.\n      check_update:\n        label: Оновлення програмного забезпечення\n        text: Автоматично перевіряти оновлення\n    interface:\n      page_title: Інтерфейс\n      language:\n        label: Мова інтерфейсу\n        msg: Мова інтерфейсу не може бути пустою.\n        text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку.\n      time_zone:\n        label: Часовий пояс\n        msg: Часовий пояс не може бути пустим.\n        text: Виберіть місто в тому ж часовому поясі, що й ви.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: З електронної пошти\n        msg: Поле з електронної пошти не може бути пустим.\n        text: Адреса електронної пошти, з якої надсилаються листи.\n      from_name:\n        label: Від імені\n        msg: Поле від імені не може бути пустим.\n        text: Ім'я, з якого надсилаються електронні листи.\n      smtp_host:\n        label: SMTP-хост\n        msg: SMTP хост не може бути порожнім.\n        text: Ваш поштовий сервер.\n      encryption:\n        label: Шифрування\n        msg: Поле шифрування не може бути пустим.\n        text: Для більшості серверів SSL є рекомендованим параметром.\n        ssl: SSL\n        tls: TLS\n        none: Нічого\n      smtp_port:\n        label: SMTP порт\n        msg: SMTP порт має бути числом 1 ~ 65535.\n        text: Порт на ваш поштовий сервер.\n      smtp_username:\n        label: Ім'я користувача SMTP\n        msg: Ім'я користувача SMTP не може бути порожнім.\n      smtp_password:\n        label: Пароль SMTP\n        msg: Пароль до SMTP не може бути порожнім.\n      test_email_recipient:\n        label: Тест отримувачів електронної пошти\n        text: Вкажіть адресу електронної пошти, на яку будуть надходити тестові надсилання.\n        msg: Тест отримувачів електронної пошти не вірний\n      smtp_authentication:\n        label: Увімкнути автентифікацію\n        title: SMTP аутентифікація\n        msg: SMTP аутентифікація не може бути порожньою.\n        \"yes\": \"Так\"\n        \"no\": \"Ні\"\n    branding:\n      page_title: Брендинг\n      logo:\n        label: Логотип\n        msg: Логотип не може бути порожнім.\n        text: Зображення логотипу у верхньому лівому кутку вашого сайту. Використовуйте широке прямокутне зображення з висотою 56 і співвідношенням сторін більше 3:1. Якщо залишити це поле порожнім, буде показано текст заголовка сайту.\n      mobile_logo:\n        label: Мобільний логотип\n        text: Логотип, що використовується на мобільній версії вашого сайту. Використовуйте широке прямокутне зображення висотою 56. Якщо залишити поле порожнім, буде використано зображення з налаштування \"логотип\".\n      square_icon:\n        label: Квадратна іконка\n        msg: Квадратна іконка не може бути пустою.\n        text: Зображення, що використовується як основа для іконок метаданих. В ідеалі має бути більшим за 512x512.\n      favicon:\n        label: Favicon\n        text: Іконка для вашого сайту. Для коректної роботи через CDN має бути у форматі png. Буде змінено розмір до 32x32. Якщо залишити порожнім, буде використовуватися \"квадратна іконка\".\n    legal:\n      page_title: Правила та умови\n      terms_of_service:\n        label: Умови використання\n        text: \"Ви можете додати вміст про умови використання тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу.\"\n      privacy_policy:\n        label: Політика конфіденційности\n        text: \"Ви можете додати вміст політики конфіденційності тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Відповідь на запис\n        label: Кожен користувач може написати лише одну відповідь на кожне запитання\n        text: \"Вимкнути, щоб дозволити користувачам писати кілька відповідей на одне і те ж питання, що може призвести до розфокусування відповідей.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Рекомендовані теги\n        text: \"За замовчуванням рекомендовані теги будуть показані у спадному списку.\"\n        msg:\n          contain_reserved: \"рекомендовані теги не можуть містити зарезервовані теги\"\n      required_tag:\n        title: Встановіть необхідні теги\n        label: Встановіть “Рекомендовані теги” як необхідні теги\n        text: \"Кожне нове питання повинно мати принаймні один рекомендований тег.\"\n      reserved_tags:\n        label: Зарезервовані теги\n        text: \"Зарезервовані теги можуть використовуватися лише модератором.\"\n      image_size:\n        label: Максимальний розмір зображення (МБ)\n        text: \"Максимальний розмір вивантаженого зображення.\"\n      attachment_size:\n        label: Максимальний розмір вкладення (МБ)\n        text: \"Максимальний розмір вкладених файлів для вивантаження.\"\n      image_megapixels:\n        label: Максимальна кількість мегапікселів зображення\n        text: \"Максимальна кількість мегапікселів, дозволена для зображення.\"\n      image_extensions:\n        label: Дозволені розширення зображень\n        text: \"Список розширень файлів, дозволених для показу зображень, через кому.\"\n      attachment_extensions:\n        label: Авторизовані розширення вкладень\n        text: \"Список дозволених для вивантаження розширень файлів, розділених комами. ПОПЕРЕДЖЕННЯ: Дозвіл на вивантаження може спричинити проблеми з безпекою.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Постійне посилання\n        text: Користувацькі структури URL можуть покращити уміння та сумісність з надсиланням посилань.\n      robots:\n        label: robots.txt\n        text: Це назавжди замінить будь-які відповідні налаштування сайту.\n    themes:\n      page_title: Теми\n      themes:\n        label: Теми\n        text: Виберіть наявну тему.\n      color_scheme:\n        label: Схема кольорів\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Основний колір\n        text: Змінюйте кольори, що використовуються у ваших темах\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS та HTML\n      custom_css:\n        label: Користувацький CSS\n        text: >\n\n      head:\n        label: Головний\n        text: >\n\n      header:\n        label: Заголовок\n        text: >\n\n      footer:\n        label: Низ\n        text: Це вставить перед &lt;/body>.\n      sidebar:\n        label: Бічна панель\n        text: Це буде вставлено в бічну панель.\n    login:\n      page_title: Увійти\n      membership:\n        title: Членство\n        label: Дозволити нові реєстрації\n        text: Вимкнути, щоб ніхто не міг створити новий обліковий запис.\n      email_registration:\n        title: Реєстрація за електронною поштою\n        label: Дозволити реєстрацію за електронною поштою\n        text: Вимкніть, щоб запобігти створенню нових облікових записів через електронну пошту.\n      allowed_email_domains:\n        title: Дозволені домени електронної пошти\n        text: Домени електронної пошти, на які користувачі повинні зареєструвати облікові записи. Один домен у рядку. Ігнорується, якщо порожній.\n      private:\n        title: Приватний\n        label: Вхід обов'язковий\n        text: Доступ до цієї спільноти мають лише зареєстровані користувачі.\n      password_login:\n        title: Вхід через пароль\n        label: Дозволити вхід через електронну пошту і пароль\n        text: \"ПОПЕРЕДЖЕННЯ: Якщо вимкнути, ви не зможете увійти в систему, якщо раніше не налаштували інший метод входу.\"\n    installed_plugins:\n      title: Встановлені плагіни\n      plugin_link: Плагіни розширюють і поглиблюють функціональність. Ви можете знайти плагіни у <1>Сховищі плагінів</1>.\n      filter:\n        all: Усі\n        active: Активні\n        inactive: Неактивні\n        outdated: Застарілі\n      plugins:\n        label: Плагіни\n        text: Виберіть наявний плагін.\n      name: Ім’я\n      version: Версія\n      status: Статус\n      action: Дія\n      deactivate: Деактивувати\n      activate: Активувати\n      settings: Налаштування\n    settings_users:\n      title: Користувачі\n      avatar:\n        label: Аватар за замовчуванням\n        text: Для користувачів без аватара власного.\n      gravatar_base_url:\n        label: Основна URL Gravatar\n        text: URL бази API постачальника Gravatar. Ігнорується, якщо порожній.\n      profile_editable:\n        title: Профіль можна редагувати\n      allow_update_display_name:\n        label: Дозволити користувачам змінювати ім'я для відображення\n      allow_update_username:\n        label: Дозволити користувачам змінювати своє ім'я користувача\n      allow_update_avatar:\n        label: Дозволити користувачам змінювати зображення свого профілю\n      allow_update_bio:\n        label: Дозволити користувачам змінювати дані про себе\n      allow_update_website:\n        label: Дозволити користувачам змінювати свій вебсайт\n      allow_update_location:\n        label: Дозволити користувачам змінювати своє місцеперебування\n    privilege:\n      title: Привілеї\n      level:\n        label: Рівень репутації необхідний\n        text: Виберіть репутацію, необхідну для привілеїв\n      msg:\n        should_be_number: введення має бути числом\n        number_larger_1: число має бути рівним або більшим за 1\n    badges:\n      action: Дія\n      active: Активні\n      activate: Активувати\n      all: Усі\n      awards: Нагороди\n      deactivate: Деактивувати\n      filter:\n        placeholder: Фільтрувати за іменем, значок:id\n      group: Група\n      inactive: Неактивні\n      name: Ім’я\n      show_logs: Показати записи журналу\n      status: Статус\n      title: Значки\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (необов'язково)\n    empty: не може бути порожнім\n    invalid: недійсне\n    btn_submit: Зберегти\n    not_found_props: \"Необхідний параметр {{ key }} не знайдено.\"\n    select: Вибрати\n  page_review:\n    review: Огляд\n    proposed: запропоновано\n    question_edit: Редагування питання\n    answer_edit: Редагування відповіді\n    tag_edit: Редагування тегу\n    edit_summary: Редагувати звіт\n    edit_question: Редагувати питання\n    edit_answer: Редагувати відповідь\n    edit_tag: Редагувати тег\n    empty: Не залишилось завдань огляду.\n    approve_revision_tip: Ви схвалюєте цю редакцію?\n    approve_flag_tip: Ви схвалюєте цю відмітку?\n    approve_post_tip: Ви схвалюєте цей допис?\n    approve_user_tip: Ви схвалюєте цього користувача?\n    suggest_edits: Запропоновані зміни\n    flag_post: Відмітити публікацію\n    flag_user: Відмітити користувача\n    queued_post: Черговий допис\n    queued_user: Черговий користувач\n    filter_label: Тип\n    reputation: репутація\n    flag_post_type: Відмічено цей пост як {{ type }}.\n    flag_user_type: Відмічено цього користувача як {{ type }}.\n    edit_post: Редагувати допис\n    list_post: Додати допис до списку\n    unlist_post: Видалити допис зі списку\n  timeline:\n    undeleted: не видалений\n    deleted: видалений\n    downvote: голос \"проти\"\n    upvote: голос \"за\"\n    accept: прийняти\n    cancelled: скасовано\n    commented: прокоментовано\n    rollback: відкат назад\n    edited: відредаговано\n    answered: дано відповідь\n    asked: запитано\n    closed: закрито\n    reopened: знову відкрито\n    created: створено\n    pin: закріплено\n    unpin: відкріплено\n    show: додано до списку\n    hide: не внесено до списку\n    title: \"Історія для\"\n    tag_title: \"Хронологія для\"\n    show_votes: \"Показати голоси\"\n    n_or_a: Н/Д\n    title_for_question: \"Хронологія для\"\n    title_for_answer: \"Часова шкала для відповіді на {{ title }} від {{ author }}\"\n    title_for_tag: \"Часова шкала для тега\"\n    datetime: Дата й час\n    type: Тип\n    by: Від\n    comment: Коментар\n    no_data: \"Ми не змогли нічого знайти.\"\n  users:\n    title: Користувачі\n    users_with_the_most_reputation: Користувачі з найвищою репутацією на цьому тижні\n    users_with_the_most_vote: Користувачі, які голосували за найбільше цього тижня\n    staffs: Персонал нашої спільноти\n    reputation: репутація\n    votes: голоси\n  prompt:\n    leave_page: Ви дійсно хочете покинути сторінку?\n    changes_not_save: Ваші зміни можуть не зберегтися.\n  draft:\n    discard_confirm: Ви дійсно бажаєте скасувати чернетку?\n  messages:\n    post_deleted: Цей допис було видалено.\n    post_cancel_deleted: Цей допис було не видалено.\n    post_pin: Цей допис було закріплено.\n    post_unpin: Цей допис було відкріплено.\n    post_hide_list: Цей допис було приховано зі списку.\n    post_show_list: Цей допис було показано у списку.\n    post_reopen: Цей допис було знову відкрито.\n    post_list: Цей допис було додано до списку.\n    post_unlist: Цей допис було приховано.\n    post_pending: Ваш допис очікує на розгляд. Це попередній перегляд, його буде видно після того, як його буде схвалено.\n    post_closed: Ця публікація була закрита.\n    answer_deleted: Ця відповідь була видалена.\n    answer_cancel_deleted: Ця відповідь була не видалена.\n    change_user_role: Роль цього користувача було змінено.\n    user_inactive: Цей користувач вже неактивний.\n    user_normal: Цей користувач вже нормальний.\n    user_suspended: Цього користувача було відсторонено.\n    user_deleted: Цього користувача було видалено.\n    user_added: User has been added successfully.\n    badge_activated: Цей бейдж було активовано.\n    badge_inactivated: Цей бейдж було деактивовано.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/vi_VN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: Thành công.\n    unknown:\n      other: Lỗi không xác định.\n    request_format_error:\n      other: Định dạng yêu cầu không hợp lệ.\n    unauthorized_error:\n      other: Chưa được cấp quyền.\n    database_error:\n      other: Lỗi dữ liệu máy chủ.\n    forbidden_error:\n      other: Bị cấm.\n    duplicate_request_error:\n      other: Trùng lặp yêu cầu.\n  action:\n    report:\n      other: Gắn nhãn\n    edit:\n      other: Chỉnh sửa\n    delete:\n      other: Xóa\n    close:\n      other: Đóng\n    reopen:\n      other: Mở lại\n    forbidden_error:\n      other: Bị cấm.\n    pin:\n      other: Ghim\n    hide:\n      other: Gỡ bỏ khỏi danh sách\n    unpin:\n      other: Bỏ ghim\n    show:\n      other: Hiển thị\n    invite_someone_to_answer:\n      other: Chỉnh sửa\n    undelete:\n      other: Khôi phục\n    merge:\n      other: Merge\n  role:\n    name:\n      user:\n        other: Người dùng\n      admin:\n        other: Quản trị viên\n      moderator:\n        other: Người điều hành\n    description:\n      user:\n        other: Mặc định không có quyền truy cập đặc biệt.\n      admin:\n        other: Có toàn quyền truy cập vào trang.\n      moderator:\n        other: Có quyền truy cập vào tất cả bài viết trừ cài đặt quản trị.\n  privilege:\n    level_1:\n      description:\n        other: Cấp độ 1 (yêu cầu danh tiếng thấp cho nhóm riêng, nhóm)\n    level_2:\n      description:\n        other: Cấp độ 2 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển)\n    level_3:\n      description:\n        other: Cấp độ 3 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển)\n    level_custom:\n      description:\n        other: Cấp độ tùy chỉnh\n    rank_question_add_label:\n      other: Đặt câu hỏi\n    rank_answer_add_label:\n      other: Viết câu trả lời\n    rank_comment_add_label:\n      other: Viết bình luận\n    rank_report_add_label:\n      other: Gắn Cờ\n    rank_comment_vote_up_label:\n      other: Bình chọn lên cho bình luận\n    rank_link_url_limit_label:\n      other: Đăng nhiều hơn 2 liên kết cùng một lúc\n    rank_question_vote_up_label:\n      other: Bình chọn lên cho câu hỏi\n    rank_answer_vote_up_label:\n      other: Bình chọn lên cho câu trả lời\n    rank_question_vote_down_label:\n      other: Bình chọn xuống cho câu hỏi\n    rank_answer_vote_down_label:\n      other: Bình chọn xuống cho câu trả lời\n    rank_invite_someone_to_answer_label:\n      other: Mời ai đó trả lời\n    rank_tag_add_label:\n      other: Tạo thẻ mới\n    rank_tag_edit_label:\n      other: Chỉnh sửa mô tả thẻ (cần xem xét)\n    rank_question_edit_label:\n      other: Chỉnh sửa câu hỏi của người khác (cần xem xét)\n    rank_answer_edit_label:\n      other: Chỉnh sửa câu trả lời của người khác (cần xem xét)\n    rank_question_edit_without_review_label:\n      other: Chỉnh sửa câu hỏi của người khác không cần xem xét\n    rank_answer_edit_without_review_label:\n      other: Chỉnh sửa câu trả lời của người khác không cần xem xét\n    rank_question_audit_label:\n      other: Xem xét chỉnh sửa câu hỏi\n    rank_answer_audit_label:\n      other: Xem xét chỉnh sửa câu trả lời\n    rank_tag_audit_label:\n      other: Xem xét chỉnh sửa thẻ\n    rank_tag_edit_without_review_label:\n      other: Chỉnh sửa mô tả thẻ không cần xem xét\n    rank_tag_synonym_label:\n      other: Quản lý từ đồng nghĩa của thẻ\n  email:\n    other: Email\n  e_mail:\n    other: Email\n  password:\n    other: Mật khẩu\n  pass:\n    other: Mật khẩu\n  old_pass:\n    other: Current password\n  original_text:\n    other: Bài viết này\n  email_or_password_wrong_error:\n    other: Email và mật khẩu không trùng khớp.\n  error:\n    common:\n      invalid_url:\n        other: URL không tồn tại.\n      status_invalid:\n        other: Trạng thái không hợp lệ\n    password:\n      space_invalid:\n        other: Mật khẩu không thể tồn tại khoảng trắng.\n    admin:\n      cannot_update_their_password:\n        other: Bạn không thể thay đổi mật khẩu.\n      cannot_edit_their_profile:\n        other: Bạn không thể thay đổi hồ sơ.\n      cannot_modify_self_status:\n        other: Bạn không thể thay đổi trạng thái của mình.\n      email_or_password_wrong:\n        other: Email và mật khẩu không khớp.\n    answer:\n      not_found:\n        other: Không tìm thấy câu trả lời.\n      cannot_deleted:\n        other: Không có quyền xóa.\n      cannot_update:\n        other: Không có quyền cập nhật.\n      question_closed_cannot_add:\n        other: Câu hỏi đã đóng và không thể thêm.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: Không được phép chỉnh sửa bình luận.\n      not_found:\n        other: Không tìm thấy bình luận.\n      cannot_edit_after_deadline:\n        other: Thời gian bình luận đã quá lâu để chỉnh sửa.\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: Email đã được dùng.\n      need_to_be_verified:\n        other: Email cần được xác minh.\n      verify_url_expired:\n        other: URL xác minh email đã hết hạn, vui lòng gửi lại email.\n      illegal_email_domain_error:\n        other: Email không được phép từ miền email đó. Vui lòng sử dụng miền khác.\n    lang:\n      not_found:\n        other: Không tìm thấy file ngôn ngữ.\n    object:\n      captcha_verification_failed:\n        other: Xác minh Captcha thất bại.\n      disallow_follow:\n        other: Bạn không được phép theo dõi.\n      disallow_vote:\n        other: Bạn không được phép bỏ phiếu.\n      disallow_vote_your_self:\n        other: Bạn không thể bỏ phiếu cho bài đăng của chính mình.\n      not_found:\n        other: Đối tượng không tìm thấy.\n      verification_failed:\n        other: Xác thực không thành công.\n      email_or_password_incorrect:\n        other: Email và mật khẩu không trùng khớp.\n      old_password_verification_failed:\n        other: Xác minh mật khẩu cũ thất bại.\n      new_password_same_as_previous_setting:\n        other: Mật khẩu mới giống như cài đặt trước.\n      already_deleted:\n        other: Mật khẩu mới giống như cài đặt trước.\n    meta:\n      object_not_found:\n        other: Đối tượng không tìm thấy\n    question:\n      already_deleted:\n        other: Bài đăng này đã bị xóa.\n      under_review:\n        other: Bài đăng của bạn đang chờ xem xét. Nó sẽ hiển thị sau khi được phê duyệt.\n      not_found:\n        other: Không tìm thấy câu hỏi.\n      cannot_deleted:\n        other: Không có quyền xóa.\n      cannot_close:\n        other: Không có quyền đóng.\n      cannot_update:\n        other: Không có quyền cập nhật.\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Xếp hạng danh tiếng không đạt được điều kiện.\n      vote_fail_to_meet_the_condition:\n        other: Cảm ơn phản hồi của bạn. Bạn cần ít nhất {{.Rank}} danh tiếng để bỏ phiếu.\n      no_enough_rank_to_operate:\n        other: Bạn cần ít nhất {{.Rank}} danh tiếng để làm điều này.\n    report:\n      handle_failed:\n        other: Xử lý báo cáo thất bại.\n      not_found:\n        other: Không tìm thấy báo cáo.\n    tag:\n      already_exist:\n        other: Thẻ đã tồn tại.\n      not_found:\n        other: Không tìm thấy thẻ.\n      recommend_tag_not_found:\n        other: Thẻ đề xuất không tồn tại.\n      recommend_tag_enter:\n        other: Vui lòng nhập ít nhất một thẻ bắt buộc.\n      not_contain_synonym_tags:\n        other: Không nên chứa các thẻ đồng nghĩa.\n      cannot_update:\n        other: Không có quyền cập nhật.\n      is_used_cannot_delete:\n        other: Bạn không thể xóa thẻ đang được sử dụng.\n      cannot_set_synonym_as_itself:\n        other: Bạn không thể đặt từ đồng nghĩa của thẻ hiện tại là chính nó.\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: Tên người gửi không thể là địa chỉ email.\n    theme:\n      not_found:\n        other: Chủ đề không tìm thấy.\n    revision:\n      review_underway:\n        other: Không thể chỉnh sửa hiện tại, có một phiên bản đang trong hàng đợi xem xét.\n      no_permission:\n        other: Không có quyền sửa đổi.\n    user:\n      external_login_missing_user_id:\n        other: Nền tảng bên thứ ba không cung cấp UserID duy nhất, vì vậy bạn không thể đăng nhập, vui lòng liên hệ với quản trị viên trang web.\n      external_login_unbinding_forbidden:\n        other: Vui lòng đặt mật khẩu đăng nhập cho tài khoản của bạn trước khi bạn gỡ bỏ đăng nhập này.\n      email_or_password_wrong:\n        other:\n          other: Email và mật khẩu không khớp.\n      not_found:\n        other: Không tìm thấy người dùng.\n      suspended:\n        other: Người dùng đã bị đình chỉ.\n      username_invalid:\n        other: Tên người dùng không hợp lệ.\n      username_duplicate:\n        other: Tên người dùng đã được sử dụng.\n      set_avatar:\n        other: Thiết lập hình đại diện thất bại.\n      cannot_update_your_role:\n        other: Bạn không thể sửa đổi vai trò của mình.\n      not_allowed_registration:\n        other: Hiện tại trang không mở đăng ký.\n      not_allowed_login_via_password:\n        other: Hiện tại trang không cho phép đăng nhập qua mật khẩu.\n      access_denied:\n        other: Truy cập bị từ chối\n      page_access_denied:\n        other: Bạn không có quyền truy cập trang này.\n      add_bulk_users_format_error:\n        other: \"Lỗi định dạng {{.Field}} gần '{{.Content}}' tại dòng {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"Số lượng người dùng bạn thêm cùng một lúc nên nằm trong khoảng từ 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: Đọc cấu hình thất bại\n    database:\n      connection_failed:\n        other: Kết nối cơ sở dữ liệu thất bại\n      create_table_failed:\n        other: Tạo bảng thất bại\n    install:\n      create_config_failed:\n        other: Không thể tạo file config.yaml.\n    upload:\n      unsupported_file_format:\n        other: Định dạng tệp không được hỗ trợ.\n    site_info:\n      config_not_found:\n        other: Không tìm thấy cấu hình trang.\n    badge:\n      object_not_found:\n        other: Đối tượng không tìm thấy\n  reason:\n    spam:\n      name:\n        other: thư rác\n      desc:\n        other: Bài đăng này quảng cáo hoặc phá hoại. Nó không hữu ích hoặc liên quan đến chủ đề hiện tại.\n    rude_or_abusive:\n      name:\n        other: thô lỗ hoặc lạm dụng\n      desc:\n        other: \"Một người hợp lý sẽ thấy nội dung này không phù hợp để diễn thuyết một cách tôn trọng.\"\n    a_duplicate:\n      name:\n        other: một bản sao\n      desc:\n        other: Câu hỏi này đã được hỏi trước đó, đã có câu trả lời.\n      placeholder:\n        other: Nhập liên kết câu hỏi hiện tại\n    not_a_answer:\n      name:\n        other: không phải câu trả lời\n      desc:\n        other: \"Điều này đã được đăng dưới dạng câu trả lời nhưng nó không cố gắng trả lời câu hỏi. Nó có thể là một bản chỉnh sửa, một nhận xét, một câu hỏi khác hoặc bị xóa hoàn toàn.\"\n    no_longer_needed:\n      name:\n        other: không còn cần thiết\n      desc:\n        other: Bình luận này đã lỗi thời, đối thoại hoặc không liên quan đến bài đăng này.\n    something:\n      name:\n        other: điều gì đó khác\n      desc:\n        other: Bài đăng này cần sự chú ý của nhân viên vì một lý do khác không được liệt kê ở trên.\n      placeholder:\n        other: Hãy cho chúng tôi biết cụ thể điều gì bạn quan tâm\n    community_specific:\n      name:\n        other: một lý do cụ thể của cộng đồng\n      desc:\n        other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng.\n    not_clarity:\n      name:\n        other: cần chi tiết hoặc rõ ràng\n      desc:\n        other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó nên tập trung vào một vấn đề duy nhất.\n    looks_ok:\n      name:\n        other: trông ổn\n      desc:\n        other: Bài đăng này tốt như vậy và không kém chất lượng.\n    needs_edit:\n      name:\n        other: cần chỉnh sửa, và tôi đã làm điều đó\n      desc:\n        other: Cải thiện và sửa các vấn đề với bài đăng này bằng chính bạn.\n    needs_close:\n      name:\n        other: cần đóng\n      desc:\n        other: Một câu hỏi đã đóng không thể trả lời, nhưng vẫn có thể chỉnh sửa, bỏ phiếu và bình luận.\n    needs_delete:\n      name:\n        other: cần xóa\n      desc:\n        other: Bài đăng này sẽ bị xóa.\n  question:\n    close:\n      duplicate:\n        name:\n          other: spam\n        desc:\n          other: Câu hỏi này đã được hỏi trước đó và đã có câu trả lời.\n      guideline:\n        name:\n          other: một lý do cụ thể của cộng đồng\n        desc:\n          other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng.\n      multiple:\n        name:\n          other: cần chi tiết hoặc rõ ràng\n        desc:\n          other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó chỉ nên tập trung vào một vấn đề.\n      other:\n        name:\n          other: điều gì đó khác\n        desc:\n          other: Bài đăng này cần một lý do khác không được liệt kê ở trên.\n    operation_type:\n      asked:\n        other: đã hỏi\n      answered:\n        other: đã trả lời\n      modified:\n        other: đã chỉnh sửa\n    deleted_title:\n      other: Câu hỏi đã xóa\n    questions_title:\n      other: Các câu hỏi\n  tag:\n    tags_title:\n      other: Thẻ\n    no_description:\n      other: Thẻ không có mô tả.\n  notification:\n    action:\n      update_question:\n        other: câu hỏi đã cập nhật\n      answer_the_question:\n        other: đã trả lời câu hỏi\n      update_answer:\n        other: câu trả lời đã cập nhật\n      accept_answer:\n        other: câu trả lời đã chấp nhận\n      comment_question:\n        other: đã bình luận câu hỏi\n      comment_answer:\n        other: đã bình luận câu trả lời\n      reply_to_you:\n        other: đã trả lời bạn\n      mention_you:\n        other: đã nhắc đến bạn\n      your_question_is_closed:\n        other: Câu hỏi của bạn đã được đóng\n      your_question_was_deleted:\n        other: Câu hỏi của bạn đã bị xóa\n      your_answer_was_deleted:\n        other: Câu trả lời của bạn đã bị xóa\n      your_comment_was_deleted:\n        other: Bình luận của bạn đã bị xóa\n      up_voted_question:\n        other: câu hỏi đã bình chọn lên\n      down_voted_question:\n        other: câu hỏi đã bình chọn xuống\n      up_voted_answer:\n        other: câu trả lời đã bình chọn lên\n      down_voted_answer:\n        other: câu trả lời đã bình chọn xuống\n      up_voted_comment:\n        other: bình luận đã bình chọn lên\n      invited_you_to_answer:\n        other: đã mời bạn trả lời\n      earned_badge:\n        other: Bạn đã nhận được huy hiệu \"{{.BadgeName}}\"\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Xác nhận địa chỉ email mới của bạn\"\n      body:\n        other: \"Xác nhận địa chỉ email mới của bạn cho {{.SiteName}} bằng cách nhấp vào liên kết sau:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nNếu bạn không yêu cầu thay đổi này, vui lòng bỏ qua email này.<br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} đã trả lời câu hỏi của bạn\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>Xem trên {{.SiteName}}</a><br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Hủy đăng ký</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} mời bạn trả lời\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>Tôi nghĩ bạn có thể biết câu trả lời.</blockquote><br>\\n<a href='{{.InviteUrl}}'>Xem trên {{.SiteName}}</a><br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Hủy đăng ký</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} đã bình luận về bài đăng của bạn\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>Xem trên {{.SiteName}}</a><br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Hủy đăng ký</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] Câu hỏi mới: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName}}] Đặt lại mật khẩu\"\n      body:\n        other: \"Ai đó đã yêu cầu đặt lại mật khẩu của bạn trên {{.SiteName}}.<br><br>\\n\\nNếu người đó không phải là bạn thì bạn có thể yên tâm bỏ qua email này.<br><br>\\n\\nNhấp vào liên kết sau để chọn mật khẩu mới:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Xác nhận tài khoản mới của bạn\"\n      body:\n        other: \"Chào mừng bạn đến với {{.SiteName}}!<br><br>\\n\\nNhấp vào liên kết sau để xác nhận và kích hoạt tài khoản mới của bạn:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nNếu liên kết trên không nhấp vào được, hãy thử sao chép và dán nó vào thanh địa chỉ trình duyệt web của bạn.\\n<br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Email kiểm tra\"\n      body:\n        other: \"Đây là một email thử nghiệm.\\n<br><br>\\n\\n--<br>\\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.\"\n  action_activity_type:\n    upvote:\n      other: bình chọn lên\n    upvoted:\n      other: đã bình chọn lên\n    downvote:\n      other: bình chọn xuống\n    downvoted:\n      other: đã bình chọn xuống\n    accept:\n      other: chấp nhận\n    accepted:\n      other: đã chấp nhận\n    edit:\n      other: chỉnh sửa\n  review:\n    queued_post:\n      other: Bài đăng trong hàng đợi\n    flagged_post:\n      other: Bài đăng được đánh dấu\n    suggested_post_edit:\n      other: Đề xuất chỉnh sửa\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} và {{ .Count }} thêm...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Tác giả tự truyện\n        desc:\n          other: Đã điền thông tin <a href=\"{{ .ProfileURL }}\" target=\"_blank\">hồ sơ</a>.\n      certified:\n        name:\n          other: Đã xác minh\n        desc:\n          other: Hoàn thành hướng dẫn cho người dùng mới của chúng tôi.\n      editor:\n        name:\n          other: Trình chỉnh sửa\n        desc:\n          other: Chỉnh sửa bài đăng đầu tiên.\n      first_flag:\n        name:\n          other: Cờ đầu tiên\n        desc:\n          other: Lần đầu tiên báo cáo một bài viết.\n      first_upvote:\n        name:\n          other: Lượt thích đầu tiên\n        desc:\n          other: Lần đầu tiên báo cáo một bài viết.\n      first_link:\n        name:\n          other: Liên kết đầu tiên\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: Phản ứng đầu tiên\n        desc:\n          other: Phản ứng với bài viết đầu tiên.\n      first_share:\n        name:\n          other: Chia sẻ đầu tiên\n        desc:\n          other: Lần đầu chia sẻ một bài viết.\n      scholar:\n        name:\n          other: Học giả\n        desc:\n          other: Đặt một câu hỏi và chấp nhận một câu trả lời.\n      commentator:\n        name:\n          other: Bình luận viên\n        desc:\n          other: Để lại 5 bình luận.\n      new_user_of_the_month:\n        name:\n          other: Người dùng mới của tháng\n        desc:\n          other: Đóng góp nổi bật trong tháng đầu tiên của họ.\n      read_guidelines:\n        name:\n          other: Đọc hướng dẫn\n        desc:\n          other: Đọc [nguyên tắc cộng đồng].\n      reader:\n        name:\n          other: Người đọc\n        desc:\n          other: Đọc mọi câu trả lời trong một chủ đề có hơn 10 câu trả lời.\n      welcome:\n        name:\n          other: Xin chào\n        desc:\n          other: Đã nhận được phiếu tán thành.\n      nice_share:\n        name:\n          other: Chia sẻ hay\n        desc:\n          other: Đã chia sẻ một bài đăng với 25 khách truy cập.\n      good_share:\n        name:\n          other: Chia sẻ tốt\n        desc:\n          other: Đã chia sẻ một bài đăng với 300 khách truy cập.\n      great_share:\n        name:\n          other: Chia sẻ tuyệt vời\n        desc:\n          other: Đã chia sẻ một bài đăng với 1000 khách truy cập.\n      out_of_love:\n        name:\n          other: Hết yêu thích\n        desc:\n          other: Đã sử dụng 50 phiếu bầu trong một ngày.\n      higher_love:\n        name:\n          other: Thích cao hơn\n        desc:\n          other: Đã sử dụng 50 phiếu bầu trong một ngày.\n      crazy_in_love:\n        name:\n          other: Thích điên cuồng\n        desc:\n          other: Đã sử dụng 50 phiếu bầu trong một ngày 20 lần.\n      promoter:\n        name:\n          other: Người quảng bá\n        desc:\n          other: Đã mời một người dùng.\n      campaigner:\n        name:\n          other: Chiến dịch\n        desc:\n          other: Đã mời 3 người dùng cơ bản.\n      champion:\n        name:\n          other: Vô địch\n        desc:\n          other: Mời 5 thành viên.\n      thank_you:\n        name:\n          other: Cảm ơn bạn\n        desc:\n          other: Có 20 bài đăng được bình chọn đưa ra 10 phiếu bầu.\n      gives_back:\n        name:\n          other: Trả lại\n        desc:\n          other: Có 100 bài đăng được bình chọn và đưa ra 100 phiếu bầu.\n      empathetic:\n        name:\n          other: Đồng cảm\n        desc:\n          other: Có 500 bài đăng được bình chọn đưa ra 1000 phiếu bầu.\n      enthusiast:\n        name:\n          other: Người nhiệt thành\n        desc:\n          other: Đã truy cập 10 ngày liên tiếp.\n      aficionado:\n        name:\n          other: Người hâm mộ\n        desc:\n          other: Đã truy cập 100 ngày liên tiếp.\n      devotee:\n        name:\n          other: Tín đồ\n        desc:\n          other: Đã truy cập 365 ngày liên tiếp.\n      anniversary:\n        name:\n          other: Kỉ niệm\n        desc:\n          other: Thành viên tích cực trong một năm, đăng ít nhất một lần.\n      appreciated:\n        name:\n          other: Đánh giá cao\n        desc:\n          other: Nhận được 1 lượt bình chọn cho 20 bài viết.\n      respected:\n        name:\n          other: Tôn trọng\n        desc:\n          other: Nhận được 2 lượt bình chọn cho 100 bài viết.\n      admired:\n        name:\n          other: Ngưỡng mộ\n        desc:\n          other: Nhận được 5 lượt bình chọn trên 300 bài đăng.\n      solved:\n        name:\n          other: Đã giải quyết\n        desc:\n          other: Có một câu trả lời được chấp nhận.\n      guidance_counsellor:\n        name:\n          other: Cố vấn hướng dẫn\n        desc:\n          other: Có 10 câu trả lời được chấp nhận.\n      know_it_all:\n        name:\n          other: Biết tất cả\n        desc:\n          other: Có 50 câu trả lời được chấp nhận.\n      solution_institution:\n        name:\n          other: Viện giải pháp\n        desc:\n          other: Có 150 câu trả lời được chấp nhận.\n      nice_answer:\n        name:\n          other: Câu trả lời tốt\n        desc:\n          other: Điểm trả lời từ 10 trở lên.\n      good_answer:\n        name:\n          other: Câu trả lời của bạn\n        desc:\n          other: Điểm trả lời từ 25 trở lên.\n      great_answer:\n        name:\n          other: Câu trả lời tuyệt vời\n        desc:\n          other: Điểm trả lời từ 50 trở lên.\n      nice_question:\n        name:\n          other: Câu trả lời tốt\n        desc:\n          other: Điểm trả lời từ 10 trở lên.\n      good_question:\n        name:\n          other: Câu trả lời tốt\n        desc:\n          other: Điểm trả lời từ 25 trở lên.\n      great_question:\n        name:\n          other: Câu trả lời tốt\n        desc:\n          other: Điểm trả lời từ 50 trở lên.\n      popular_question:\n        name:\n          other: Câu hỏi phổ biến\n        desc:\n          other: Câu hỏi với 500 lượt xem.\n      notable_question:\n        name:\n          other: Câu hỏi đáng chú ý\n        desc:\n          other: Câu hỏi với 1.000 lượt xem.\n      famous_question:\n        name:\n          other: Câu hỏi nổi tiếng\n        desc:\n          other: Câu hỏi với 5.000 lượt xem.\n      popular_link:\n        name:\n          other: Liên kết phổ biến\n        desc:\n          other: Đã đăng một liên kết bên ngoài với 50 lần nhấp chuột.\n      hot_link:\n        name:\n          other: Liên kết nổi bật\n        desc:\n          other: Đã đăng một liên kết bên ngoài với 300 lần nhấp chuột.\n      famous_link:\n        name:\n          other: Liên kết nổi tiếng\n        desc:\n          other: Đã đăng một liên kết bên ngoài với 100 lần nhấp chuột.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Bắt đầu\n      community:\n        name:\n          other: Cộng đồng\n      posting:\n        name:\n          other: Viết bài thảo luận\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: Cách định dạng\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">đề cập đến bài đăng: <code>#post_id</code></p></li> <li><p class =\"mb-2\">để tạo liên kết</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\">đặt trả về giữa đoạn văn</p></li><li><p class=\"mb-2\"><em>_italic_</em> hoặc **<strong>in đậm</strong>**</p></li ><li><p class=\"mb-2\">mã thụt lề 4 dấu cách</p></li><li><p class=\"mb-2\">trích dẫn bằng cách đặt <code>&gt;</ code> ở đầu dòng</p></li><li><p class=\"mb-2\">backtick thoát <code>`like _this_`</code></p></li><li><p class=\"mb-2\">tạo hàng rào mã bằng dấu backticks <code>`</code></p><pre class=\"mb -0\"><code>```<br/>mã vào đây<br/>```</code></pre></li></ul>\n  pagination:\n    prev: Trước\n    next: Tiếp\n  page_title:\n    question: Câu hỏi\n    questions: Các câu hỏi\n    tag: Thẻ\n    tags: Các thẻ\n    tag_wiki: wiki thẻ\n    create_tag: Tạo thẻ\n    edit_tag: Chỉnh sửa thẻ\n    ask_a_question: Create Question\n    edit_question: Chỉnh sửa câu hỏi\n    edit_answer: Chỉnh sửa câu\n    search: Tìm kiếm\n    posts_containing: Bài đăng chứa\n    settings: Cài đặt\n    notifications: Các thông báo\n    login: Đăng nhập\n    sign_up: Đăng ký\n    account_recovery: Khôi phục tài khoản\n    account_activation: Kích hoạt tài khoản\n    confirm_email: Xác nhận Email\n    account_suspended: Tài khoản bị đình chỉ\n    admin: Quản trị\n    change_email: Thay đổi Email\n    install: Cài đặt Answer\n    upgrade: Nâng cấp Answer\n    maintenance: Bảo trì trang web\n    users: Người dùng\n    oauth_callback: Đang xử lý\n    http_404: Lỗi HTTP 404\n    http_50X: Lỗi HTTP 500\n    http_403: Lỗi HTTP 403\n    logout: Đăng xuất\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: Các thông báo\n    inbox: Hộp thư đến\n    achievement: Thành tích\n    new_alerts: Cảnh báo mới\n    all_read: Đánh dấu tất cả đã đọc\n    show_more: Xem thêm\n    someone: Ai đó\n    inbox_type:\n      all: Tất cả\n      posts: Bài đăng\n      invites: Lời mời\n      votes: Bình chọn\n    answer: Câu trả lời\n    question: Câu hỏi\n    badge_award: Huy hiệu\n  suspended:\n    title: Tài khoản của bạn đã bị đình chỉ\n    until_time: \"Tài khoản của bạn đã bị đình chỉ cho đến {{ time }}.\"\n    forever: Người dùng này đã bị đình chỉ vĩnh viễn.\n    end: Bạn không tuân thủ hướng dẫn cộng đồng.\n    contact_us: Liên hệ với chúng tôi\n  editor:\n    blockquote:\n      text: Trích dẫn\n    bold:\n      text: Đậm\n    chart:\n      text: Biểu đồ\n      flow_chart: Biểu đồ luồng\n      sequence_diagram: Sơ đồ trình tự\n      class_diagram: Sơ đồ lớp\n      state_diagram: Sơ đồ trạng thái\n      entity_relationship_diagram: Sơ đồ quan hệ thực thể\n      user_defined_diagram: Sơ đồ do người dùng định nghĩa\n      gantt_chart: Biểu đồ Gantt\n      pie_chart: Biểu đồ tròn\n    code:\n      text: Mẫu code\n      add_code: Thêm code mẫu\n      form:\n        fields:\n          code:\n            label: Mã\n            msg:\n              empty: Mã không thể trống.\n          language:\n            label: Ngôn ngữ\n            placeholder: Phát hiện tự động\n      btn_cancel: Hủy\n      btn_confirm: Thêm\n    formula:\n      text: Công thức\n      options:\n        inline: Công thức nội dòng\n        block: Công thức khối\n    heading:\n      text: Tiêu đề\n      options:\n        h1: Tiêu đề 1\n        h2: Tiêu đề 2\n        h3: Tiêu đề 3\n        h4: Tiêu đề 4\n        h5: Tiêu đề 5\n        h6: Tiêu đề 6\n    help:\n      text: Trợ giúp\n    hr:\n      text: Thước ngang\n    image:\n      text: Hình ảnh\n      add_image: Thêm hình ảnh\n      tab_image: Tải Ảnh lên\n      form_image:\n        fields:\n          file:\n            label: Tệp hình ảnh\n            btn: Chọn hình ảnh\n            msg:\n              empty: Tệp không thể trống.\n              only_image: Chỉ cho phép tệp hình ảnh.\n              max_size: Kích thước tệp không được vượt quá {{size}} MB.\n          desc:\n            label: Mô tả\n      tab_url: URL hình ảnh\n      form_url:\n        fields:\n          url:\n            label: URL hình ảnh\n            msg:\n              empty: URL hình ảnh không thể trống.\n          name:\n            label: Mô tả\n      btn_cancel: Hủy\n      btn_confirm: Thêm\n      uploading: Đang tải lên\n    indent:\n      text: Canh lề\n    outdent:\n      text: Lùi lề\n    italic:\n      text: Nhấn mạnh\n    link:\n      text: Liên kết\n      add_link: Thêm liên kết\n      form:\n        fields:\n          url:\n            label: Đường link url\n            msg:\n              empty: URL không thể trống.\n          name:\n            label: Mô tả\n      btn_cancel: Hủy\n      btn_confirm: Thêm\n    ordered_list:\n      text: Danh sách đánh số\n    unordered_list:\n      text: Danh sách gạch đầu dòng\n    table:\n      text: Bảng\n      heading: Tiêu đề\n      cell: Ô\n    file:\n      text: Đính kèm tập tin\n      not_supported: \"Không hỗ trợ loại tệp đó. Hãy thử lại với {{file_type}}.\"\n      max_size: \"Kích thước tệp đính kèm không được vượt quá {{size}} MB.\"\n  close_modal:\n    title: Tôi đang đóng bài đăng này với lý do...\n    btn_cancel: Hủy\n    btn_submit: Gửi\n    remark:\n      empty: Không thể trống.\n    msg:\n      empty: Vui lòng chọn một lý do.\n  report_modal:\n    flag_title: Tôi đang đánh dấu để báo cáo bài đăng này với lý do...\n    close_title: Tôi đang đóng bài đăng này với lý do...\n    review_question_title: Xem xét câu hỏi\n    review_answer_title: Xem xét câu trả lời\n    review_comment_title: Xem xét bình luận\n    btn_cancel: Hủy\n    btn_submit: Gửi\n    remark:\n      empty: Không thể trống.\n    msg:\n      empty: Vui lòng chọn một lý do.\n      not_a_url: Định dạng URL không chính xác.\n      url_not_match: Nguồn gốc URL không khớp với trang web hiện tại.\n  tag_modal:\n    title: Tạo thẻ mới\n    form:\n      fields:\n        display_name:\n          label: Tên hiển thị\n          msg:\n            empty: Tên hiển thị không thể trống.\n            range: Tên hiển thị tối đa 35 ký tự.\n        slug_name:\n          label: Đường dẫn URL\n          desc: Đường dẫn tối đa 35 ký tự.\n          msg:\n            empty: Đường dẫn URL không thể trống.\n            range: Đường dẫn URL tối đa 35 ký tự.\n            character: Đường dẫn URL chứa bộ ký tự không được phép.\n        desc:\n          label: Mô tả\n        revision:\n          label: Sửa đổi\n        edit_summary:\n          label: Tóm tắt chỉnh sửa\n          placeholder: >-\n            Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng)\n    btn_cancel: Hủy\n    btn_submit: Gửi\n    btn_post: Đăng thẻ mới\n  tag_info:\n    created_at: Đã tạo\n    edited_at: Đã chỉnh sửa\n    history: Lịch sử\n    synonyms:\n      title: Từ đồng nghĩa\n      text: Các thẻ sau sẽ được ánh xạ lại thành\n      empty: Không tìm thấy từ đồng nghĩa.\n      btn_add: Thêm từ đồng nghĩa\n      btn_edit: Chỉnh sửa\n      btn_save: Lưu\n    synonyms_text: Các thẻ sau sẽ được ánh xạ lại thành\n    delete:\n      title: Xóa thẻ này\n      tip_with_posts: >-\n        <p>Chúng tôi không cho phép <strong>xóa thẻ có bài đăng</strong>.</p> <p>Vui lòng xóa thẻ này khỏi các bài đăng trước.</p>\n      tip_with_synonyms: >-\n        <p>Chúng tôi không cho phép <strong>xóa thẻ có từ đồng nghĩa</strong>.</p> <p>Vui lòng xóa các từ đồng nghĩa khỏi thẻ này trước.</p>\n      tip: Bạn có chắc chắn muốn xóa không?\n      close: Đóng\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: Submit\n      btn_close: Close\n  edit_tag:\n    title: Chỉnh sửa Thẻ\n    default_reason: Chỉnh sửa thẻ\n    default_first_reason: Thêm thẻ\n    btn_save_edits: Lưu chỉnh sửa\n    btn_cancel: Hủy\n  dates:\n    long_date: MMM D\n    long_date_with_year: \"MMM D, YYYY\"\n    long_date_with_time: \"MMM D, YYYY [at] HH:mm\"\n    now: bây giờ\n    x_seconds_ago: \"{{count}}giây trước\"\n    x_minutes_ago: \"{{count}}phút trước\"\n    x_hours_ago: \"{{count}}giờ trước\"\n    hour: giờ\n    day: ngày\n    hours: giờ\n    days: ngày\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: trái tim\n    smile: nụ cười\n    frown: nhăn mặt\n    btn_label: thêm hoặc loại bỏ phản ứng\n    undo_emoji: bỏ dấu {{ emoji }} phản ứng\n    react_emoji: biểu cảm với {{ emoji }}\n    unreact_emoji: hủy biểu cảm {{ emoji }}\n  comment:\n    btn_add_comment: Thêm bình luận\n    reply_to: Trả lời cho\n    btn_reply: Trả lời\n    btn_edit: Chỉnh sửa\n    btn_delete: Xóa\n    btn_flag: Gắn Cờ\n    btn_save_edits: Lưu chỉnh sửa\n    btn_cancel: Hủy\n    show_more: \"{{count}} bình luận khác\"\n    tip_question: >-\n      Sử dụng bình luận để yêu cầu thêm thông tin hoặc đề xuất cải tiến. Tránh trả lời câu hỏi trong bình luận.\n    tip_answer: >-\n      Sử dụng bình luận để trả lời cho người dùng khác hoặc thông báo cho họ về các thay đổi. Nếu bạn đang thêm thông tin mới, hãy chỉnh sửa bài đăng của mình thay vì bình luận.\n    tip_vote: Nó thêm điều gì đó hữu ích cho bài đăng\n  edit_answer:\n    title: Chỉnh sửa Câu trả lời\n    default_reason: Chỉnh sửa câu trả lời\n    default_first_reason: Thêm câu trả lời\n    form:\n      fields:\n        revision:\n          label: Sửa đổi\n        answer:\n          label: Câu trả lời\n          feedback:\n            characters: nội dung phải có ít nhất 6 ký tự.\n        edit_summary:\n          label: Tóm tắt chỉnh sửa\n          placeholder: >-\n            Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng)\n    btn_save_edits: Lưu chỉnh sửa\n    btn_cancel: Hủy\n  tags:\n    title: Thẻ\n    sort_buttons:\n      popular: Phổ biến\n      name: Tên\n      newest: Mới nhất\n    button_follow: Theo dõi\n    button_following: Đang theo dõi\n    tag_label: câu hỏi\n    search_placeholder: Lọc theo tên thẻ\n    no_desc: Thẻ không có mô tả.\n    more: Thêm\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: Chỉnh sửa Câu hỏi\n    default_reason: Chỉnh sửa câu hỏi\n    default_first_reason: Create question\n    similar_questions: Câu hỏi tương tự\n    form:\n      fields:\n        revision:\n          label: Sửa đổi\n        title:\n          label: Tiêu đề\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: Tiêu đề không thể trống.\n            range: Tiêu đề tối đa 150 ký tự\n        body:\n          label: Nội dung\n          msg:\n            empty: Nội dung không thể trống.\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: Thẻ\n          msg:\n            empty: Thẻ không thể trống.\n        answer:\n          label: Câu trả lời\n          msg:\n            empty: Câu trả lời không thể trống.\n        edit_summary:\n          label: Tóm tắt chỉnh sửa\n          placeholder: >-\n            Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng)\n    btn_post_question: Đăng câu hỏi của bạn\n    btn_save_edits: Lưu chỉnh sửa\n    answer_question: Trả lời câu hỏi của chính bạn\n    post_question&answer: Đăng câu hỏi và câu trả lời của bạn\n  tag_selector:\n    add_btn: Thêm thẻ\n    create_btn: Tạo thẻ mới\n    search_tag: Tìm kiếm thẻ\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: Không có thẻ phù hợp\n    tag_required_text: Thẻ bắt buộc (ít nhất một)\n  header:\n    nav:\n      question: Câu hỏi\n      tag: Thẻ\n      user: Người dùng\n      badges: Danh hiệu\n      profile: Hồ sơ\n      setting: Cài đặt\n      logout: Đăng xuất\n      admin: Quản trị\n      review: Xem xét\n      bookmark: Đánh dấu\n      moderation: Điều hành\n    search:\n      placeholder: Tìm kiếm\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: Thay đổi\n    loading: đang tải...\n  pic_auth_code:\n    title: Mã xác minh\n    placeholder: Nhập văn bản ở trên\n    msg:\n      empty: Captcha không thể trống.\n  inactive:\n    first: >-\n      Bạn gần như đã hoàn tất! Chúng tôi đã gửi một email kích hoạt đến <bold>{{mail}}</bold>. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn.\n    info: \"Nếu không nhận được, hãy kiểm tra thư mục spam của bạn.\"\n    another: >-\n      Chúng tôi đã gửi một email kích hoạt khác cho bạn tại <bold>{{mail}}</bold>. Có thể mất vài phút để nó đến; hãy chắc chắn kiểm tra thư mục thư rác của bạn.\n    btn_name: Gửi lại email kích hoạt\n    change_btn_name: Thay đổi email\n    msg:\n      empty: Không thể để trống mục này.\n    resend_email:\n      url_label: Bạn có chắc chắn muốn gửi lại email kích hoạt không?\n      url_text: Bạn cũng có thể cung cấp liên kết kích hoạt ở trên cho người dùng.\n  login:\n    login_to_continue: Đăng nhập để tiếp tục\n    info_sign: Bạn không có tài khoản? <1>Đăng ký</1>\n    info_login: Bạn đã có tài khoản? <1>Đăng nhập</1>\n    agreements: Bằng cách đăng ký, bạn đồng ý với <1>chính sách bảo mật</1> và <3>điều khoản dịch vụ</3>.\n    forgot_pass: Quên mật khẩu?\n    name:\n      label: Tên\n      msg:\n        empty: Tên không thể trống.\n        range: Tên phải có độ dài từ 2 đến 30 ký tự.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: Email\n      msg:\n        empty: Email không thể trống.\n    password:\n      label: Mật khẩu\n      msg:\n        empty: Mật khẩu không thể trống.\n        different: Mật khẩu nhập vào ở hai bên không nhất quán\n  account_forgot:\n    page_title: Quên mật khẩu\n    btn_name: Gửi email khôi phục cho tôi\n    send_success: >-\n      Nếu một tài khoản khớp với <strong>{{mail}}</strong>, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình.\n    email:\n      label: Email\n      msg:\n        empty: Email không thể trống.\n  change_email:\n    btn_cancel: Hủy\n    btn_update: Cập nhật địa chỉ email\n    send_success: >-\n      Nếu một tài khoản khớp với <strong>{{mail}}</strong>, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình.\n    email:\n      label: Email mới\n      msg:\n        empty: Email không thể trống.\n  oauth:\n    connect: Kết nối với {{ auth_name }}\n    remove: Xóa bỏ {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Thêm email khôi phục vào tài khoản của bạn.\n    btn_update: Cập nhật địa chỉ email\n    email:\n      label: Email\n      msg:\n        empty: Email không thể trống.\n    modal_title: Email đã tồn tại.\n    modal_content: Địa chỉ email này đã được đăng ký. Bạn có chắc chắn muốn kết nối với tài khoản hiện tại không?\n    modal_cancel: Thay đổi email\n    modal_confirm: Kết nối với tài khoản hiện tại\n  password_reset:\n    page_title: Đặt lại mật khẩu\n    btn_name: Đặt lại mật khẩu của tôi\n    reset_success: >-\n      Bạn đã thay đổi mật khẩu thành công; bạn sẽ được chuyển hướng đến trang đăng nhập.\n    link_invalid: >-\n      Xin lỗi, liên kết đặt lại mật khẩu này không còn hợp lệ. Có thể mật khẩu của bạn đã được đặt lại?\n    to_login: Tiếp tục đến trang đăng nhập\n    password:\n      label: Mật khẩu\n      msg:\n        empty: Mật khẩu không thể trống.\n        length: Độ dài cần nằm trong khoảng từ 8 đến 32\n        different: Mật khẩu nhập vào ở hai bên không nhất quán\n    password_confirm:\n      label: Xác nhận mật khẩu mới\n  settings:\n    page_title: Cài đặt\n    goto_modify: Đi đến sửa đổi\n    nav:\n      profile: Hồ sơ\n      notification: Thông báo\n      account: Tài khoản\n      interface: Giao diện\n    profile:\n      heading: Hồ sơ\n      btn_name: Lưu\n      display_name:\n        label: Tên hiển thị\n        msg: Tên hiển thị không thể trống.\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: Tên người dùng\n        caption: Mọi người có thể nhắc đến bạn với \"@username\".\n        msg: Tên người dùng không thể trống.\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Hình ảnh hồ sơ\n        gravatar: Gravatar\n        gravatar_text: Bạn có thể thay đổi hình ảnh trên\n        custom: Tùy chỉnh\n        custom_text: Bạn có thể tải lên hình ảnh của mình.\n        default: Hệ thống\n        msg: Vui lòng tải lên một hình đại diện\n      bio:\n        label: Giới thiệu về tôi\n      website:\n        label: Website\n        placeholder: \"https://example.com\"\n        msg: Định dạng website không chính xác\n      location:\n        label: Địa điểm\n        placeholder: \"Thành phố, Quốc gia\"\n    notification:\n      heading: Thông báo qua Email\n      turn_on: Bật\n      inbox:\n        label: Thông báo hộp thư đến\n        description: Các câu trả lời cho câu hỏi của bạn, bình luận, lời mời và nhiều hơn nữa.\n      all_new_question:\n        label: Tất cả câu hỏi mới\n        description: Nhận thông báo về tất cả các câu hỏi mới. Tối đa 50 câu hỏi mỗi tuần.\n      all_new_question_for_following_tags:\n        label: Tất cả câu hỏi mới cho các thẻ theo dõi\n        description: Nhận thông báo về các câu hỏi mới cho các thẻ đang theo dõi.\n    account:\n      heading: Tài khoản\n      change_email_btn: Thay đổi email\n      change_pass_btn: Thay đổi mật khẩu\n      change_email_info: >-\n        Chúng tôi đã gửi một email đến địa chỉ đó. Vui lòng làm theo hướng dẫn xác nhận.\n      email:\n        label: Email\n      new_email:\n        label: Email mới\n        msg: Email mới không được để trống.\n      pass:\n        label: Mật khẩu hiện tại\n        msg: Mật khẩu không thể trống.\n      password_title: Mật khẩu\n      current_pass:\n        label: Mật khẩu hiện tại\n        msg:\n          empty: Mật khẩu hiện tại không thể trống.\n          length: Độ dài cần nằm trong khoảng từ 8 đến 32.\n          different: Hai mật khẩu nhập vào không khớp.\n      new_pass:\n        label: Mật khẩu mới\n      pass_confirm:\n        label: Xác nhận mật khẩu mới\n    interface:\n      heading: Giao diện\n      lang:\n        label: Ngôn ngữ giao diện\n        text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang.\n    my_logins:\n      title: Đăng nhập của tôi\n      label: Đăng nhập hoặc đăng ký trên trang này bằng các tài khoản này.\n      modal_title: Xóa đăng nhập\n      modal_content: Bạn có chắc chắn muốn xóa đăng nhập này khỏi tài khoản của bạn không?\n      modal_confirm_btn: Xóa\n      remove_success: Đã xóa thành công\n  toast:\n    update: cập nhật thành công\n    update_password: Mật khẩu đã được thay đổi thành công.\n    flag_success: Cảm ơn bạn đã đánh dấu.\n    forbidden_operate_self: Không được phép thao tác trên chính mình\n    review: Sửa đổi của bạn sẽ được hiển thị sau khi được xem xét.\n    sent_success: Đã gửi thành công\n  related_question:\n    title: Related\n    answers: câu trả lời\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: Mời mọi người\n    desc: Mời những người bạn nghĩ có thể trả lời.\n    invite: Mời trả lời\n    add: Thêm người\n    search: Tìm kiếm người\n  question_detail:\n    action: Hành động\n    created: Created\n    Asked: Đã hỏi\n    asked: đã hỏi\n    update: Đã chỉnh sửa\n    Edited: Edited\n    edit: đã chỉnh sửa\n    commented: đã bình luận\n    Views: Lượt xem\n    Follow: Theo dõi\n    Following: Đang theo dõi\n    follow_tip: Theo dõi câu hỏi này để nhận thông báo\n    answered: đã trả lời\n    closed_in: Đóng trong\n    show_exist: Hiển thị câu hỏi hiện tại.\n    useful: Hữu ích\n    question_useful: Nó hữu ích và rõ ràng\n    question_un_useful: Nó không rõ ràng hoặc không hữu ích\n    question_bookmark: Đánh dấu câu hỏi này\n    answer_useful: Nó hữu ích\n    answer_un_useful: Nó không hữu ích\n    answers:\n      title: Các câu trả lời\n      score: Điểm\n      newest: Mới nhất\n      oldest: Cũ nhất\n      btn_accept: Chấp nhận\n      btn_accepted: Đã chấp nhận\n    write_answer:\n      title: Câu trả lời của bạn\n      edit_answer: Chỉnh sửa câu trả lời hiện tại của tôi\n      btn_name: Đăng câu trả lời của bạn\n      add_another_answer: Thêm câu trả lời khác\n      confirm_title: Tiếp tục trả lời\n      continue: Tiếp tục\n      confirm_info: >-\n        <p>Bạn có chắc chắn muốn thêm một câu trả lời khác không?</p><p>Bạn có thể sử dụng liên kết chỉnh sửa để tinh chỉnh và cải thiện câu trả lời hiện tại của mình, thay vì.</p>\n      empty: Câu trả lời không thể trống.\n      characters: nội dung phải có ít nhất 6 ký tự.\n      tips:\n        header_1: Cảm ơn câu trả lời của bạn\n        li1_1: Vui lòng chắc chắn <strong>trả lời câu hỏi</strong>. Cung cấp chi tiết và chia sẻ nghiên cứu của bạn.\n        li1_2: Hỗ trợ bất kỳ tuyên bố nào bạn đưa ra với tài liệu tham khảo hoặc kinh nghiệm cá nhân.\n        header_2: Nhưng <strong>tránh</strong> ...\n        li2_1: Yêu cầu trợ giúp, yêu cầu làm rõ, hoặc trả lời cho các câu trả lời khác.\n    reopen:\n      confirm_btn: Mở lại\n      title: Mở lại bài đăng này\n      content: Bạn có chắc chắn muốn mở lại không?\n    list:\n      confirm_btn: Danh sách\n      title: Danh sách bài đăng này\n      content: Bạn có chắc chắn muốn liệt kê không?\n    unlist:\n      confirm_btn: Gỡ bỏ khỏi danh sách\n      title: Gỡ bỏ bài đăng này khỏi danh sách\n      content: Bạn có chắc chắn muốn gỡ bỏ không?\n    pin:\n      title: Ghim bài đăng này\n      content: Bạn có chắc chắn muốn ghim toàn cầu không? Bài đăng này sẽ xuất hiện ở đầu tất cả các danh sách bài đăng.\n      confirm_btn: Ghim\n  delete:\n    title: Xóa bài đăng này\n    question: >-\n      Chúng tôi không khuyến khích <strong>xóa câu hỏi có câu trả lời</strong> vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai.</p><p>Việc xóa liên tục các câu hỏi đã được trả lời có thể dẫn đến việc tài khoản của bạn bị chặn không được phép hỏi. Bạn có chắc chắn muốn xóa không?\n    answer_accepted: >-\n      <p>Chúng tôi không khuyến khích <strong>xóa câu trả lời đã được chấp nhận</strong> vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai. </p> Việc xóa liên tục các câu trả lời đã được chấp nhận có thể dẫn đến việc tài khoản của bạn bị chặn không được phép trả lời. Bạn có chắc chắn muốn xóa không?\n    other: Bạn có chắc chắn muốn xóa không?\n    tip_answer_deleted: Câu trả lời này đã bị xóa\n    undelete_title: Khôi phục bài đăng này\n    undelete_desc: Bạn có chắc chắn muốn khôi phục không?\n  btns:\n    confirm: Xác nhận\n    cancel: Hủy\n    edit: Chỉnh sửa\n    save: Lưu\n    delete: Xóa\n    undelete: Khôi phục\n    list: Danh sách\n    unlist: Gỡ bỏ khỏi danh sách\n    unlisted: Không được liệt kê\n    login: Đăng nhập\n    signup: Đăng ký\n    logout: Đăng xuất\n    verify: Xác minh\n    create: Create\n    approve: Phê duyệt\n    reject: Từ chối\n    skip: Bỏ qua\n    discard_draft: Hủy bản nháp\n    pinned: Đã ghim\n    all: Tất cả\n    question: Câu hỏi\n    answer: Câu trả lời\n    comment: Bình luận\n    refresh: Làm mới\n    resend: Gửi lại\n    deactivate: Ngừng kích hoạt\n    active: Hoạt động\n    suspend: Tạm ngừng\n    unsuspend: Bỏ vô hiệu hóa\n    close: Đóng\n    reopen: Mở lại\n    ok: Đồng ý\n    light: Phông nền sáng\n    dark: Tối\n    system_setting: Cài đặt hệ thống\n    default: Mặc định\n    reset: Đặt lại\n    tag: Thẻ\n    post_lowercase: bài đăng\n    filter: Lọc\n    ignore: Bỏ qua\n    submit: Gửi\n    normal: Bình thường\n    closed: Đã đóng\n    deleted: Đã xóa\n    deleted_permanently: Deleted permanently\n    pending: Đang chờ xử lý\n    more: Thêm\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: Kết quả tìm kiếm\n    keywords: Từ khóa\n    options: Tùy chọn\n    follow: Theo dõi\n    following: Đang theo dõi\n    counts: \"{{count}} Kết quả\"\n    counts_loading: \"... Results\"\n    more: Thêm\n    sort_btns:\n      relevance: Liên quan\n      newest: Mới nhất\n      active: Hoạt động\n      score: Điểm\n      more: Thêm\n    tips:\n      title: Mẹo tìm kiếm nâng cao\n      tag: \"<1>[tag]</1> tìm kiếm trong một thẻ\"\n      user: \"<1>user:username</1> tìm kiếm theo tác giả\"\n      answer: \"<1>answers:0</1> câu hỏi chưa có câu trả lời\"\n      score: \"<1>score:3</1> bài đăng có điểm 3+\"\n      question: \"<1>is:question</1> tìm kiếm câu hỏi\"\n      is_answer: \"<1>is:answer</1> tìm kiếm câu trả lời\"\n    empty: Chúng tôi không thể tìm thấy bất cứ thứ gì. <br /> Thử các từ khóa khác hoặc ít cụ thể hơn.\n  share:\n    name: Chia sẻ\n    copy: Sao chép liên kết\n    via: Chia sẻ bài đăng qua...\n    copied: Đã sao chép\n    facebook: Chia sẻ lên Facebook\n    twitter: Share to X\n  cannot_vote_for_self: Bạn không thể bỏ phiếu cho bài đăng của chính mình.\n  modal_confirm:\n    title: Lỗi...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: Tài khoản mới của bạn đã được xác nhận; bạn sẽ được chuyển hướng đến trang chủ.\n    link: Tiếp tục đến trang chủ\n    oops: Rất tiếc!\n    invalid: Liên kết bạn đã dùng không còn hoạt động nữa.\n    confirm_new_email: Email của bạn đã được cập nhật.\n    confirm_new_email_invalid: >-\n      Xin lỗi, liên kết xác nhận này không còn hợp lệ. Có thể email của bạn đã được thay đổi?\n  unsubscribe:\n    page_title: Hủy đăng ký\n    success_title: Hủy đăng ký thành công\n    success_desc: Bạn đã được gỡ bỏ khỏi danh sách người đăng ký này và sẽ không nhận được thêm email từ chúng tôi.\n    link: Thay đổi cài đặt\n  question:\n    following_tags: Thẻ đang theo dõi\n    edit: Chỉnh sửa\n    save: Lưu\n    follow_tag_tip: Theo dõi các thẻ để tùy chỉnh danh sách câu hỏi của bạn.\n    hot_questions: Câu hỏi nổi bật\n    all_questions: Tất cả câu hỏi\n    x_questions: \"{{ count }} Câu hỏi\"\n    x_answers: \"{{ count }} câu trả lời\"\n    x_posts: \"{{ count }} Posts\"\n    questions: Câu hỏi\n    answers: Câu trả lời\n    newest: Mới nhất\n    active: Hoạt động\n    hot: Được nhiều quan tâm\n    frequent: Thường xuyên\n    recommend: Đề xuất\n    score: Điểm\n    unanswered: Chưa được trả lời\n    modified: đã chỉnh sửa\n    answered: đã trả lời\n    asked: đã hỏi\n    closed: đã đóng\n    follow_a_tag: Theo dõi một thẻ\n    more: Thêm\n  personal:\n    overview: Tổng quan\n    answers: Câu trả lời\n    answer: câu trả lời\n    questions: Câu hỏi\n    question: câu hỏi\n    bookmarks: Đánh dấu\n    reputation: Danh tiếng\n    comments: Bình luận\n    votes: Bình chọn\n    badges: Danh hiệu\n    newest: Mới nhất\n    score: Điểm\n    edit_profile: Chỉnh sửa hồ sơ\n    visited_x_days: \"Đã truy cập {{ count }} ngày\"\n    viewed: Đã xem\n    joined: Tham gia\n    comma: \",\"\n    last_login: Đã xem\n    about_me: Về tôi\n    about_me_empty: \"// Xin chào, Thế giới !\"\n    top_answers: Câu trả lời hàng đầu\n    top_questions: Câu hỏi hàng đầu\n    stats: Thống kê\n    list_empty: Không tìm thấy bài đăng.<br />Có thể bạn muốn chọn một thẻ khác?\n    content_empty: Không tìm thấy bài viết nào.\n    accepted: Đã chấp nhận\n    answered: đã trả lời\n    asked: đã hỏi\n    downvoted: đã bỏ phiếu xuống\n    mod_short: MOD\n    mod_long: Người điều hành\n    x_reputation: danh tiếng\n    x_votes: phiếu bầu nhận được\n    x_answers: câu trả lời\n    x_questions: câu hỏi\n    recent_badges: Huy hiệu gần đây\n  install:\n    title: Cài đặt\n    next: Tiếp theo\n    done: Hoàn thành\n    config_yaml_error: Không thể tạo file config.yaml.\n    lang:\n      label: Vui lòng chọn một ngôn ngữ\n    db_type:\n      label: Hệ quản trị cơ sở dữ liệu\n    db_username:\n      label: Tên người dùng\n      placeholder: root\n      msg: Tên người dùng không thể trống.\n    db_password:\n      label: Mật khẩu\n      placeholder: root\n      msg: Mật khẩu không thể trống.\n    db_host:\n      label: Máy chủ cơ sở dữ liệu\n      placeholder: \"db:3306\"\n      msg: Máy chủ cơ sở dữ liệu không thể trống.\n    db_name:\n      label: Tên cơ sở dữ liệu\n      placeholder: câu trả lời\n      msg: Tên cơ sở dữ liệu không thể trống.\n    db_file:\n      label: Tệp tin Database\n      placeholder: /data/answer.db\n      msg: Tệp cơ sở dữ liệu không thể trống.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: Tạo config.yaml\n      label: Tệp config.yaml đã được tạo.\n      desc: >-\n        Bạn có thể tạo tệp <1>config.yaml</1> thủ công trong thư mục <1>/var/wwww/xxx/</1> và dán văn bản sau vào đó.\n      info: Sau khi bạn đã làm xong, nhấp vào nút \"Tiếp theo\".\n    site_information: Thông tin trang\n    admin_account: Tài khoản quản trị\n    site_name:\n      label: Tên trang\n      msg: Tên trang không thể trống.\n      msg_max_length: Tên trang phải có tối đa 30 ký tự.\n    site_url:\n      label: URL trang\n      text: Địa chỉ của trang của bạn.\n      msg:\n        empty: URL trang không thể trống.\n        incorrect: Định dạng URL trang không chính xác.\n        max_length: URL trang phải có tối đa 512 ký tự.\n    contact_email:\n      label: Email liên hệ\n      text: Địa chỉ email của người liên hệ chính phụ trách trang này.\n      msg:\n        empty: Email liên hệ không thể trống.\n        incorrect: Định dạng email liên hệ không chính xác.\n    login_required:\n      label: Riêng tư\n      switch: Yêu cầu đăng nhập\n      text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này.\n    admin_name:\n      label: Tên\n      msg: Tên không thể trống.\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: Mật khẩu\n      text: >-\n        Bạn sẽ cần mật khẩu này để đăng nhập. Vui lòng lưu trữ nó ở một nơi an toàn.\n      msg: Mật khẩu không thể trống.\n      msg_min_length: Mật khẩu phải có ít nhất 8 ký tự.\n      msg_max_length: Mật khẩu phải có tối đa 32 ký tự.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: Email\n      text: Bạn sẽ cần email này để đăng nhập.\n      msg:\n        empty: Email không thể trống.\n        incorrect: Định dạng email không chính xác.\n    ready_title: Trang web của bạn đã sẵn sàng\n    ready_desc: >-\n      Nếu bạn cảm thấy muốn thay đổi thêm cài đặt nào đó, hãy truy cập <1>mục quản trị</1>; tìm nó trong menu trang.\n    good_luck: \"Chúc bạn vui vẻ và may mắn!\"\n    warn_title: Cảnh báo\n    warn_desc: >-\n      Tệp <1>config.yaml</1> đã tồn tại. Nếu bạn cần đặt lại bất kỳ mục cấu hình nào trong tệp này, vui lòng xóa nó trước.\n    install_now: Bạn có thể thử <1>cài đặt ngay bây giờ</1>.\n    installed: Đã cài đặt\n    installed_desc: >-\n      Có vẻ như bạn đã cài đặt rồi. Để cài đặt lại, vui lòng xóa các bảng cơ sở dữ liệu cũ trước.\n    db_failed: Kết nối cơ sở dữ liệu thất bại\n    db_failed_desc: >-\n      Điều này có thể có nghĩa là thông tin cơ sở dữ liệu trong tệp <1>config.yaml</1> của bạn không chính xác hoặc không thể thiết lập liên lạc với máy chủ cơ sở dữ liệu. Điều này có thể có nghĩa là máy chủ cơ sở dữ liệu của máy chủ của bạn đang bị tắt.\n  counts:\n    views: lượt xem\n    votes: bình chọn\n    answers: câu trả lời\n    accepted: Đã chấp nhận\n  page_error:\n    http_error: Lỗi HTTP {{ code }}\n    desc_403: Bạn không có quyền truy cập trang này.\n    desc_404: Thật không may, trang này không tồn tại.\n    desc_50X: Máy chủ đã gặp sự cố và không thể hoàn thành yêu cầu của bạn.\n    back_home: Quay lại trang chủ\n  page_maintenance:\n    desc: \"Chúng tôi đang bảo trì, chúng tôi sẽ trở lại sớm.\"\n  nav_menus:\n    dashboard: Bảng điều khiển\n    contents: Nội dung\n    questions: Câu hỏi\n    answers: Câu trả lời\n    users: Người dùng\n    badges: Huy hiệu\n    flags: Cờ\n    settings: Cài đặt\n    general: Chung\n    interface: Giao diện\n    smtp: SMTP\n    branding: Thương hiệu\n    legal: Pháp lý\n    write: Viết\n    terms: Terms\n    tos: Điều khoản dịch vụ\n    privacy: Quyền riêng tư\n    seo: SEO\n    customize: Tùy chỉnh\n    themes: Chủ đề\n    login: Đăng nhập\n    privileges: Đặc quyền\n    plugins: Plugins\n    installed_plugins: Plugin đã cài đặt\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Chào mừng bạn đến với {{site_name}}\n  user_center:\n    login: Đăng nhập\n    qrcode_login_tip: Vui lòng sử dụng {{ agentName }} để quét mã QR và đăng nhập.\n    login_failed_email_tip: Đăng nhập thất bại, vui lòng cho phép ứng dụng này truy cập thông tin email của bạn trước khi thử lại.\n  badges:\n    modal:\n      title: Chúc mừng\n      content: Bạn đã nhận được huy hiệu mới.\n      close: Đóng\n      confirm: Xem huy hiệu\n    title: Huy hiệu\n    awarded: Giải Thưởng\n    earned_×: Nhận được ×{{ number }}\n    ×_awarded: \"{{ number }} được trao tặng\"\n    can_earn_multiple: Bạn có thể kiếm được nhiều lần.\n    earned: Đã nhận\n  admin:\n    admin_header:\n      title: Quản trị\n    dashboard:\n      title: Bảng điều khiển\n      welcome: Chào mừng bạn đến với Answer Admin!\n      site_statistics: Thống kê trang\n      questions: \"Câu hỏi:\"\n      resolved: \"Đã giải quyết:\"\n      unanswered: \"Chưa được trả lời:\"\n      answers: \"Câu trả lời:\"\n      comments: \"Bình luận:\"\n      votes: \"Phiếu bầu:\"\n      users: \"Người dùng:\"\n      flags: \"Cờ:\"\n      reviews: \"Đánh giá:\"\n      site_health: Sức khỏe trang\n      version: \"Phiên bản:\"\n      https: \"HTTPS:\"\n      upload_folder: \"Thư mục tải lên:\"\n      run_mode: \"Chế độ hoạt động:\"\n      private: Riêng tư\n      public: Công cộng\n      smtp: \"SMTP:\"\n      timezone: \"Múi giờ:\"\n      system_info: Thông tin hệ thống\n      go_version: \"Phiên bản Go:\"\n      database: \"Database:\"\n      database_size: \"Tệp tin Database:\"\n      storage_used: \"Bộ nhớ đã sử dụng:\"\n      uptime: \"Thời gian hoạt động:\"\n      links: Links\n      plugins: Plugin\n      github: GitHub\n      blog: Blog\n      contact: Liên hệ\n      forum: Diễn đàn\n      documents: Tài liệu\n      feedback: Phản hồi\n      support: Hỗ trợ\n      review: Đánh giá\n      config: Cấu hình\n      update_to: Cập nhật lên\n      latest: Mới nhất\n      check_failed: Kiểm tra thất bại\n      \"yes\": \"Có\"\n      \"no\": \"Không\"\n      not_allowed: Không được phép\n      allowed: Được phép\n      enabled: Đã bật\n      disabled: Đã tắt\n      writable: Có thể chỉnh sửa\n      not_writable: Không thể ghi\n    flags:\n      title: Cờ\n      pending: Đang chờ xử lý\n      completed: Hoàn thành\n      flagged: Đã đánh dấu\n      flagged_type: Đã đánh dấu {{ type }}\n      created: Đã tạo\n      action: Hành động\n      review: Đánh giá\n    user_role_modal:\n      title: Thay đổi vai trò người dùng thành...\n      btn_cancel: Hủy\n      btn_submit: Gửi\n    new_password_modal:\n      title: Đặt mật khẩu mới\n      form:\n        fields:\n          password:\n            label: Mật khẩu\n            text: Người dùng sẽ bị đăng xuất và cần đăng nhập lại.\n            msg: Mật khẩu phải có độ dài từ 8 đến 32 ký tự.\n      btn_cancel: Hủy\n      btn_submit: Gửi\n    edit_profile_modal:\n      title: Chỉnh sửa hồ sơ\n      form:\n        fields:\n          display_name:\n            label: Tên hiển thị\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Tên người dùng\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Địa chỉ email không hợp lệ.\n      edit_success: Chỉnh Sửa Thành Công\n      btn_cancel: Hủy\n      btn_submit: Gửi\n    user_modal:\n      title: Thêm người dùng mới\n      form:\n        fields:\n          users:\n            label: Thêm người dùng hàng loạt\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Tách \"tên, email, mật khẩu\" bằng dấu phẩy. Một người dùng mỗi dòng.\n            msg: \"Vui lòng nhập email của người dùng, một dòng mỗi người.\"\n          display_name:\n            label: Tên hiển thị\n            msg: Tên hiển thị phải dài từ 2-30 ký tự.\n          email:\n            label: Email\n            msg: Email không hợp lệ.\n          password:\n            label: Mật khẩu\n            msg: Mật khẩu phải có từ 8 đến 32 ký tự.\n      btn_cancel: Hủy\n      btn_submit: Gửi\n    users:\n      title: Người dùng\n      name: Tên\n      email: Email\n      reputation: Danh tiếng\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: Trạng thái\n      role: Vai trò\n      action: Hành động\n      change: Thay đổi\n      all: Tất cả\n      staff: Nhân viên\n      more: Thêm\n      inactive: Không hoạt động\n      suspended: Bị tạm ngưng\n      deleted: Đã xóa\n      normal: Bình thường\n      Moderator: Người điều hành\n      Admin: Quản trị viên\n      User: Người dùng\n      filter:\n        placeholder: \"Lọc theo tên, user:id\"\n      set_new_password: Đặt mật khẩu mới\n      edit_profile: Chỉnh sửa hồ sơ\n      change_status: Thay đổi trạng thái\n      change_role: Thay đổi vai trò\n      show_logs: Hiển thị nhật ký\n      add_user: Thêm người dùng\n      deactivate_user:\n        title: Ngừng kích hoạt người dùng\n        content: Người dùng không hoạt động phải xác nhận lại email của họ.\n      delete_user:\n        title: Xóa người dùng này\n        content: Bạn có chắc chắn muốn xóa người dùng này không? Điều này là vĩnh viễn!\n        remove: Xóa nội dung của họ\n        label: Xóa tất cả các câu hỏi, câu trả lời, bình luận, vv.\n        text: Không chọn điều này nếu bạn chỉ muốn xóa tài khoản của người dùng.\n      suspend_user:\n        title: Đình chỉ người dùng này\n        content: Người dùng bị đình chỉ không thể đăng nhập.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: Câu hỏi\n      unlisted: Không được liệt kê\n      post: Bài đăng\n      votes: Phiếu bầu\n      answers: Câu trả lời\n      created: Đã tạo\n      status: Trạng thái\n      action: Hành động\n      change: Thay đổi\n      pending: Đang chờ xử lý\n      filter:\n        placeholder: \"Lọc theo tiêu đề, question:id\"\n    answers:\n      page_title: Câu trả lời\n      post: Bài đăng\n      votes: Phiếu bầu\n      created: Đã tạo\n      status: Trạng thái\n      action: Hành động\n      change: Thay đổi\n      filter:\n        placeholder: \"Lọc theo tiêu đề, answer:id\"\n    general:\n      page_title: Chung\n      name:\n        label: Tên trang\n        msg: Tên trang không thể trống.\n        text: \"Tên của trang này, được sử dụng trong thẻ tiêu đề.\"\n      site_url:\n        label: URL trang\n        msg: Url trang không thể trống.\n        validate: Vui lòng nhập URL hợp lệ.\n        text: Địa chỉ của trang của bạn.\n      short_desc:\n        label: Mô tả ngắn của trang\n        msg: Mô tả ngắn của trang không thể trống.\n        text: \"Mô tả ngắn, được sử dụng trong thẻ tiêu đề trên trang chủ.\"\n      desc:\n        label: Mô tả trang\n        msg: Mô tả trang không thể trống.\n        text: \"Mô tả trang này trong một câu, được sử dụng trong thẻ mô tả meta.\"\n      contact_email:\n        label: Email liên hệ\n        msg: Email liên hệ không thể trống.\n        validate: Định dạng email liên hệ không hợp lệ.\n        text: Địa chỉ email của người liên hệ chính phụ trách trang này.\n      check_update:\n        label: Cập nhật phần mềm\n        text: Tự động kiểm tra cập nhật\n    interface:\n      page_title: Giao diện\n      language:\n        label: Ngôn ngữ giao diện\n        msg: Ngôn ngữ giao diện không thể trống.\n        text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang.\n      time_zone:\n        label: Múi giờ\n        msg: Múi giờ không thể trống.\n        text: Chọn một thành phố cùng múi giờ với bạn.\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: Email gửi từ\n        msg: Email gửi từ không thể trống.\n        text: Địa chỉ email mà các email được gửi từ đó.\n      from_name:\n        label: Tên gửi từ\n        msg: Tên gửi từ không thể trống.\n        text: Tên mà các email được gửi từ đó.\n      smtp_host:\n        label: Máy chủ SMTP\n        msg: Máy chủ SMTP không thể trống.\n        text: Máy chủ thư của bạn.\n      encryption:\n        label: Mã hóa\n        msg: Mã hóa không thể trống.\n        text: Đối với hầu hết các máy chủ, SSL là tùy chọn được khuyến nghị.\n        ssl: SSL\n        tls: TLS\n        none: Không\n      smtp_port:\n        label: Cổng SMTP\n        msg: Cổng SMTP phải là số từ 1 đến 65535.\n        text: Cổng đến máy chủ thư của bạn.\n      smtp_username:\n        label: Tên người dùng SMTP\n        msg: Tên người dùng SMTP không thể trống.\n      smtp_password:\n        label: Mật khẩu SMTP\n        msg: Mật khẩu SMTP không thể trống.\n      test_email_recipient:\n        label: Người nhận email kiểm tra\n        text: Cung cấp địa chỉ email sẽ nhận email kiểm tra.\n        msg: Người nhận email kiểm tra không hợp lệ\n      smtp_authentication:\n        label: Bật xác thực\n        title: Xác thực SMTP\n        msg: Xác thực SMTP không thể trống.\n        \"yes\": \"Có\"\n        \"no\": \"Không\"\n    branding:\n      page_title: Thương hiệu\n      logo:\n        label: Logo\n        msg: Logo không thể trống.\n        text: Hình ảnh logo ở góc trên bên trái của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56 và tỷ lệ khung hình lớn hơn 3:1. Nếu để trống, văn bản tiêu đề trang sẽ được hiển thị.\n      mobile_logo:\n        label: Logo di động\n        text: Logo được sử dụng trên phiên bản di động của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56. Nếu để trống, hình ảnh từ cài đặt \"logo\" sẽ được sử dụng.\n      square_icon:\n        label: Biểu tượng vuông\n        msg: Biểu tượng vuông không thể trống.\n        text: Hình ảnh được sử dụng làm cơ sở cho các biểu tượng siêu dữ liệu. Nên lớn hơn 512x512.\n      favicon:\n        label: Favicon\n        text: Favicon cho trang của bạn. Để hoạt động chính xác trên một CDN, nó phải là png. Sẽ được thay đổi kích thước thành 32x32. Nếu để trống, \"biểu tượng vuông\" sẽ được sử dụng.\n    legal:\n      page_title: Pháp lý\n      terms_of_service:\n        label: Điều khoản dịch vụ\n        text: \"Bạn có thể thêm nội dung điều khoản dịch vụ ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây.\"\n      privacy_policy:\n        label: Chính sách bảo mật\n        text: \"Bạn có thể thêm nội dung chính sách bảo mật ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây.\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Câu trả lời chỉnh sửa\n        label: Each user can only write one answer for each question\n        text: \"Tắt để cho phép người dùng viết nhiều câu trả lời cho cùng một câu hỏi, điều này có thể khiến các câu trả lời bị mất trọng tâm.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Thẻ được đề xuất\n        text: \"Các thẻ gợi ý sẽ hiển thị trong danh sách thả xuống theo mặc định.\"\n        msg:\n          contain_reserved: \"các thẻ được đề xuất không được chứa thẻ dự bị\"\n      required_tag:\n        title: Đặt thẻ cần thiết\n        label: Đặt thẻ được đề xuất là bắt buộc\n        text: \"Mỗi câu hỏi mới phải có ít nhất một thẻ được đề xuất.\"\n      reserved_tags:\n        label: Thẻ dành riêng\n        text: \"Thẻ dành riêng chỉ có thể được thêm vào một bài đăng bởi điều hành viên.\"\n      image_size:\n        label: Kích thước hình ảnh tối đa (MB)\n        text: \"Kích thước tải lên hình ảnh tối đa.\"\n      attachment_size:\n        label: Kích thước tệp đính kèm tối đa (MB)\n        text: \"Kích thước tải lên tệp đính kèm tối đa.\"\n      image_megapixels:\n        label: Megapixel hình ảnh tối đa\n        text: \"Số megapixel tối đa được phép cho một hình ảnh.\"\n      image_extensions:\n        label: Tiện ích mở rộng hình ảnh được ủy quyền\n        text: \"Danh sách đuôi file được phép hiển thị hình ảnh, phân cách bằng dấu phẩy.\"\n      attachment_extensions:\n        label: Các loại tệp đính kèm được phép tải lên\n        text: \"Danh sách các đuôi file được phép tải lên, phân cách bằng dấu phẩy. CẢNH BÁO: Cho phép tải lên có thể gây ra vấn đề bảo mật.\"\n    seo:\n      page_title: SEO\n      permalink:\n        label: Liên kết cố định\n        text: Cấu trúc URL tùy chỉnh có thể cải thiện khả năng sử dụng và khả năng tương thích về sau của liên kết của bạn.\n      robots:\n        label: robots.txt\n        text: Điều này sẽ ghi đè vĩnh viễn bất kỳ cài đặt trang web liên quan nào.\n    themes:\n      page_title: Giao diện\n      themes:\n        label: Giao diện\n        text: Chọn một chủ đề hiện có.\n      color_scheme:\n        label: Sơ đồ màu\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: Màu chính\n        text: Thay đổi các màu sắc được sử dụng bởi chủ đề của bạn\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS và HTML\n      custom_css:\n        label: CSS tùy chỉnh\n        text: >\n\n      head:\n        label: Đầu\n        text: >\n\n      header:\n        label: Đầu trang\n        text: >\n\n      footer:\n        label: Cuối trang\n        text: Điều này sẽ chèn trước &lt;/body>.\n      sidebar:\n        label: Thanh bên\n        text: Điều này sẽ chèn vào thanh bên.\n    login:\n      page_title: Đăng nhập\n      membership:\n        title: Thành viên\n        label: Cho phép đăng ký mới\n        text: Tắt để ngăn ai đó tạo tài khoản mới.\n      email_registration:\n        title: Đăng ký qua email\n        label: Cho phép đăng ký qua email\n        text: Tắt để ngăn ai đó tạo tài khoản mới thông qua email.\n      allowed_email_domains:\n        title: Miền email được phép\n        text: Miền email mà người dùng phải đăng ký tài khoản. Một miền mỗi dòng. Bỏ qua khi trống.\n      private:\n        title: Riêng tư\n        label: Yêu cầu đăng nhập\n        text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này.\n      password_login:\n        title: Đăng nhập bằng mật khẩu\n        label: Cho phép đăng nhập bằng email và mật khẩu\n        text: \"CẢNH BÁO: Nếu tắt, bạn có thể không thể đăng nhập nếu bạn chưa cấu hình phương thức đăng nhập khác trước đó.\"\n    installed_plugins:\n      title: Plugin đã cài đặt\n      plugin_link: Plugin mở rộng và mở rộng chức năng của trang web. Bạn có thể tìm thấy plugin trong <1>Kho Plugin Answer</1>.\n      filter:\n        all: Tất cả\n        active: Đang hoạt động\n        inactive: Không hoạt động\n        outdated: Quá hạn\n      plugins:\n        label: Plugin\n        text: Chọn một plugin hiện có.\n      name: Tên\n      version: Phiên bản\n      status: Trạng thái\n      action: Hành động\n      deactivate: Vô hiệu hóa\n      activate: Kích hoạt\n      settings: Cài đặt\n    settings_users:\n      title: Người dùng\n      avatar:\n        label: Hình đại diện mặc định\n        text: Dành cho người dùng không có hình đại diện tùy chỉnh của riêng họ.\n      gravatar_base_url:\n        label: Gravatar Base URL\n        text: URL của nhà cung cấp API Gravatar. Bỏ qua khi trống.\n      profile_editable:\n        title: Hồ sơ có thể chỉnh sửa\n      allow_update_display_name:\n        label: Cho phép người dùng thay đổi tên hiển thị của họ\n      allow_update_username:\n        label: Cho phép người dùng thay đổi tên người dùng của họ\n      allow_update_avatar:\n        label: Cho phép người dùng thay đổi hình ảnh hồ sơ của họ\n      allow_update_bio:\n        label: Cho phép người dùng thay đổi giới thiệu về mình\n      allow_update_website:\n        label: Cho phép người dùng thay đổi trang web của họ\n      allow_update_location:\n        label: Cho phép người dùng thay đổi vị trí của họ\n    privilege:\n      title: Đặc quyền\n      level:\n        label: Mức độ danh tiếng yêu cầu\n        text: Chọn mức danh tiếng yêu cầu cho các đặc quyền\n      msg:\n        should_be_number: dữ liệu đầu vào phải là kiểu số\n        number_larger_1: số phải bằng hoặc lớn hơn 1\n    badges:\n      action: Hành động\n      active: Hoạt động\n      activate: Kích hoạt\n      all: Tất cả\n      awards: Giải Thưởng\n      deactivate: Ngừng kích hoạt\n      filter:\n        placeholder: Lọc theo tên, user:id\n      group: Nhóm\n      inactive: Không hoạt động\n      name: Tên\n      show_logs: Hiển thị nhật ký\n      status: Trạng thái\n      title: Danh hiệu\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (tùy chọn)\n    empty: không thể trống\n    invalid: không hợp lệ\n    btn_submit: Lưu\n    not_found_props: \"Không tìm thấy thuộc tính bắt buộc {{ key }}.\"\n    select: Chọn\n  page_review:\n    review: Xem xét\n    proposed: đề xuất\n    question_edit: Chỉnh sửa câu hỏi\n    answer_edit: Câu trả lời chỉnh sửa\n    tag_edit: Chỉnh sửa thẻ\n    edit_summary: Tóm tắt chỉnh sửa\n    edit_question: Chỉnh sửa câu hỏi\n    edit_answer: Chỉnh sửa câu trả lời\n    edit_tag: Chỉnh sửa thẻ\n    empty: Không còn nhiệm vụ xem xét nào.\n    approve_revision_tip: Bạn có chấp nhận sửa đổi này không?\n    approve_flag_tip: Bạn có chấp nhận cờ này không?\n    approve_post_tip: Bạn có chấp nhận bài đăng này không?\n    approve_user_tip: Bạn có chấp nhận người dùng này không?\n    suggest_edits: Đề xuất chỉnh sửa\n    flag_post: Đánh dấu bài đăng\n    flag_user: Đánh dấu người dùng\n    queued_post: Bài đăng trong hàng đợi\n    queued_user: Người dùng trong hàng đợi\n    filter_label: Loại\n    reputation: danh tiếng\n    flag_post_type: Đánh dấu bài đăng này là {{ type }}.\n    flag_user_type: Đánh dấu người dùng này là {{ type }}.\n    edit_post: Chỉnh sửa bài đăng\n    list_post: Liệt kê bài đăng\n    unlist_post: Gỡ bỏ bài đăng khỏi danh sách\n  timeline:\n    undeleted: đã khôi phục\n    deleted: đã xóa\n    downvote: bỏ phiếu xuống\n    upvote: bỏ phiếu lên\n    accept: chấp nhận\n    cancelled: đã hủy\n    commented: đã bình luận\n    rollback: quay lại\n    edited: đã chỉnh sửa\n    answered: đã trả lời\n    asked: đã hỏi\n    closed: đã đóng\n    reopened: đã mở lại\n    created: đã tạo\n    pin: đã ghim\n    unpin: bỏ ghim\n    show: được liệt kê\n    hide: không được liệt kê\n    title: \"Lịch sử cho\"\n    tag_title: \"Dòng thời gian cho\"\n    show_votes: \"Hiển thị phiếu bầu\"\n    n_or_a: N/A\n    title_for_question: \"Dòng thời gian cho\"\n    title_for_answer: \"Dòng thời gian cho câu trả lời của {{ title }} bởi {{ author }}\"\n    title_for_tag: \"Dòng thời gian cho thẻ\"\n    datetime: Ngày giờ\n    type: Loại\n    by: Bởi\n    comment: Bình luận\n    no_data: \"Chúng tôi không thể tìm thấy bất cứ thứ gì.\"\n  users:\n    title: Người dùng\n    users_with_the_most_reputation: Người dùng có điểm danh tiếng cao nhất trong tuần này\n    users_with_the_most_vote: Người dùng đã bỏ phiếu nhiều nhất trong tuần này\n    staffs: Nhân viên cộng đồng của chúng tôi\n    reputation: danh tiếng\n    votes: phiếu bầu\n  prompt:\n    leave_page: Bạn có chắc chắn muốn rời khỏi trang không?\n    changes_not_save: Các thay đổi của bạn có thể không được lưu.\n  draft:\n    discard_confirm: Bạn có chắc chắn muốn hủy bản nháp của mình không?\n  messages:\n    post_deleted: Bài đăng này đã bị xóa.\n    post_cancel_deleted: Bài đăng này đã được phục hồi.\n    post_pin: Bài đăng này đã được ghim.\n    post_unpin: Bài đăng này đã bị bỏ ghim.\n    post_hide_list: Bài đăng này đã được ẩn khỏi danh sách.\n    post_show_list: Bài đăng này đã được hiển thị trên danh sách.\n    post_reopen: Bài đăng này đã được mở lại.\n    post_list: Bài đăng này đã được liệt kê.\n    post_unlist: Bài đăng này đã được gỡ bỏ khỏi danh sách.\n    post_pending: Bài đăng của bạn đang chờ xem xét. Đây là bản xem trước, nó sẽ được hiển thị sau khi được phê duyệt.\n    post_closed: Bài đăng này đã bị đóng.\n    answer_deleted: Câu trả lời này đã bị xóa.\n    answer_cancel_deleted: Câu trả lời này đã được phục hồi.\n    change_user_role: Vai trò của người dùng này đã được thay đổi.\n    user_inactive: Người dùng này đã không hoạt động.\n    user_normal: Người dùng này đã bình thường.\n    user_suspended: Người dùng này đã bị đình chỉ.\n    user_deleted: Người dùng này đã bị xóa.\n    user_added: User has been added successfully.\n    badge_activated: Huy hiệu này đã được kích hoạt.\n    badge_inactivated: Huy hiệu này đã bị vô hiệu hóa.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "i18n/zh_CN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: 成功。\n    unknown:\n      other: 未知错误。\n    request_format_error:\n      other: 请求格式错误。\n    unauthorized_error:\n      other: 未授权。\n    database_error:\n      other: 数据服务器错误。\n    forbidden_error:\n      other: 禁止访问。\n    duplicate_request_error:\n      other: 重复提交。\n  action:\n    report:\n      other: 举报\n    edit:\n      other: 编辑\n    delete:\n      other: 删除\n    close:\n      other: 关闭\n    reopen:\n      other: 重新打开\n    forbidden_error:\n      other: 禁止访问。\n    pin:\n      other: 置顶\n    hide:\n      other: 列表隐藏\n    unpin:\n      other: 取消置顶\n    show:\n      other: 列表显示\n    invite_someone_to_answer:\n      other: 编辑\n    undelete:\n      other: 撤消删除\n    merge:\n      other: 合并\n  role:\n    name:\n      user:\n        other: 用户\n      admin:\n        other: 管理员\n      moderator:\n        other: 版主\n    description:\n      user:\n        other: 默认没有特殊权限。\n      admin:\n        other: 拥有管理网站的全部权限。\n      moderator:\n        other: 拥有除访问后台管理以外的所有权限。\n  privilege:\n    level_1:\n      description:\n        other: 级别 1（少量声望要求，适合私有团队、群组）\n    level_2:\n      description:\n        other: 级别 2（低声望要求，适合初启动的社区）\n    level_3:\n      description:\n        other: 级别 3（高声望要求，适合成熟的社区）\n    level_custom:\n      description:\n        other: 自定义等级\n    rank_question_add_label:\n      other: 提问\n    rank_answer_add_label:\n      other: 写答案\n    rank_comment_add_label:\n      other: 写评论\n    rank_report_add_label:\n      other: 举报\n    rank_comment_vote_up_label:\n      other: 点赞评论\n    rank_link_url_limit_label:\n      other: 每次发布超过 2 个链接\n    rank_question_vote_up_label:\n      other: 点赞问题\n    rank_answer_vote_up_label:\n      other: 点赞答案\n    rank_question_vote_down_label:\n      other: 点踩问题\n    rank_answer_vote_down_label:\n      other: 点踩答案\n    rank_invite_someone_to_answer_label:\n      other: 邀请回答\n    rank_tag_add_label:\n      other: 创建新标签\n    rank_tag_edit_label:\n      other: 编辑标签描述（需要审核）\n    rank_question_edit_label:\n      other: 编辑别人的问题（需要审核）\n    rank_answer_edit_label:\n      other: 编辑别人的答案（需要审核）\n    rank_question_edit_without_review_label:\n      other: 编辑别人的问题无需审核\n    rank_answer_edit_without_review_label:\n      other: 编辑别人的答案无需审核\n    rank_question_audit_label:\n      other: 审核问题编辑\n    rank_answer_audit_label:\n      other: 审核回答编辑\n    rank_tag_audit_label:\n      other: 审核标签编辑\n    rank_tag_edit_without_review_label:\n      other: 编辑标签描述无需审核\n    rank_tag_synonym_label:\n      other: 管理标签同义词\n  email:\n    other: 邮箱\n  e_mail:\n    other: 邮箱\n  password:\n    other: 密码\n  pass:\n    other: 密码\n  old_pass:\n    other: 当前密码\n  original_text:\n    other: 本帖\n  email_or_password_wrong_error:\n    other: 邮箱和密码不匹配。\n  error:\n    common:\n      invalid_url:\n        other: 无效的 URL。\n      status_invalid:\n        other: 无效状态。\n    password:\n      space_invalid:\n        other: 密码不得含有空格。\n    admin:\n      cannot_update_their_password:\n        other: 你无法修改自己的密码。\n      cannot_edit_their_profile:\n        other: 您不能修改您的个人资料。\n      cannot_modify_self_status:\n        other: 你无法修改自己的状态。\n      email_or_password_wrong:\n        other: 邮箱和密码不匹配。\n    answer:\n      not_found:\n        other: 没有找到答案。\n      cannot_deleted:\n        other: 没有删除权限。\n      cannot_update:\n        other: 没有更新权限。\n      question_closed_cannot_add:\n        other: 问题已关闭，无法添加。\n      content_cannot_empty:\n        other: 回答内容不能为空。\n    comment:\n      edit_without_permission:\n        other: 不允许编辑评论。\n      not_found:\n        other: 评论未找到。\n      cannot_edit_after_deadline:\n        other: 评论时间太久，无法修改。\n      content_cannot_empty:\n        other: 评论内容不能为空。\n    email:\n      duplicate:\n        other: 邮箱已存在。\n      need_to_be_verified:\n        other: 邮箱需要验证。\n      verify_url_expired:\n        other: 邮箱验证的网址已过期，请重新发送邮件。\n      illegal_email_domain_error:\n        other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。\n    lang:\n      not_found:\n        other: 语言文件未找到。\n    object:\n      captcha_verification_failed:\n        other: 验证码错误。\n      disallow_follow:\n        other: 你不能关注。\n      disallow_vote:\n        other: 你不能投票。\n      disallow_vote_your_self:\n        other: 你不能为自己的帖子投票。\n      not_found:\n        other: 对象未找到。\n      verification_failed:\n        other: 验证失败。\n      email_or_password_incorrect:\n        other: 邮箱和密码不匹配。\n      old_password_verification_failed:\n        other: 旧密码验证失败。\n      new_password_same_as_previous_setting:\n        other: 新密码和旧密码相同。\n      already_deleted:\n        other: 该帖子已被删除。\n    meta:\n      object_not_found:\n        other: Meta 对象未找到\n    question:\n      already_deleted:\n        other: 该帖子已被删除。\n      under_review:\n        other: 您的帖子正在等待审核。它将在它获得批准后可见。\n      not_found:\n        other: 问题未找到。\n      cannot_deleted:\n        other: 没有删除权限。\n      cannot_close:\n        other: 没有关闭权限。\n      cannot_update:\n        other: 没有更新权限。\n      content_cannot_empty:\n        other: 内容不能为空。\n      content_less_than_minimum:\n        other: 输入的内容不足。\n    rank:\n      fail_to_meet_the_condition:\n        other: 声望值未达到要求。\n      vote_fail_to_meet_the_condition:\n        other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。\n      no_enough_rank_to_operate:\n        other: 你至少需要 {{.Rank}} 声望才能执行此操作。\n    report:\n      handle_failed:\n        other: 报告处理失败。\n      not_found:\n        other: 报告未找到。\n    tag:\n      already_exist:\n        other: 标签已存在。\n      not_found:\n        other: 标签未找到。\n      recommend_tag_not_found:\n        other: 推荐标签不存在。\n      recommend_tag_enter:\n        other: 请选择至少一个必选标签。\n      not_contain_synonym_tags:\n        other: 不应包含同义词标签。\n      cannot_update:\n        other: 没有更新权限。\n      is_used_cannot_delete:\n        other: 你不能删除这个正在使用的标签。\n      cannot_set_synonym_as_itself:\n        other: 你不能将当前标签设为自己的同义词。\n      minimum_count:\n        other: 没有输入足够的标签。\n    smtp:\n      config_from_name_cannot_be_email:\n        other: 发件人名称不能是邮箱地址。\n    theme:\n      not_found:\n        other: 主题未找到。\n    revision:\n      review_underway:\n        other: 目前无法编辑，有一个版本在审阅队列中。\n      no_permission:\n        other: 无权限修改。\n    user:\n      external_login_missing_user_id:\n        other: 第三方平台没有提供唯一的 UserID，所以你不能登录，请联系网站管理员。\n      external_login_unbinding_forbidden:\n        other: 请在移除此登录之前为你的账户设置登录密码。\n      email_or_password_wrong:\n        other:\n          other: 邮箱和密码不匹配。\n      not_found:\n        other: 用户未找到。\n      suspended:\n        other: 用户已被封禁。\n      username_invalid:\n        other: 用户名无效。\n      username_duplicate:\n        other: 用户名已被使用。\n      set_avatar:\n        other: 头像设置错误。\n      cannot_update_your_role:\n        other: 你不能修改自己的角色。\n      not_allowed_registration:\n        other: 该网站暂未开放注册。\n      not_allowed_login_via_password:\n        other: 该网站暂不支持密码登录。\n      access_denied:\n        other: 拒绝访问\n      page_access_denied:\n        other: 您没有权限访问此页面。\n      add_bulk_users_format_error:\n        other: \"发生错误，{{.Field}} 格式错误，在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。\"\n      status_suspended_forever:\n        other: \"<strong>该用户已被永久封禁。</strong>该用户不符合社区准则。\"\n      status_suspended_until:\n        other: \"<strong>该用户已被封禁至 {{.SuspendedUntil}}。</strong>该用户不符合社区准则。\"\n      status_deleted:\n        other: \"该用户已被删除。\"\n      status_inactive:\n        other: \"该用户未激活。\"\n    config:\n      read_config_failed:\n        other: 读取配置失败\n    database:\n      connection_failed:\n        other: 数据库连接失败\n      create_table_failed:\n        other: 创建表失败\n    install:\n      create_config_failed:\n        other: 无法创建 config.yaml 文件。\n    upload:\n      unsupported_file_format:\n        other: 不支持的文件格式。\n    site_info:\n      config_not_found:\n        other: 未找到网站的该配置信息。\n    badge:\n      object_not_found:\n        other: 没有找到徽章对象\n  reason:\n    spam:\n      name:\n        other: 垃圾信息\n      desc:\n        other: 这个帖子是一个广告，或是破坏性行为。它对当前的主题无帮助或无关。\n    rude_or_abusive:\n      name:\n        other: 粗鲁或辱骂的\n      desc:\n        other: \"一个有理智的人都会认为这种内容不适合进行尊重性的讨论。\"\n    a_duplicate:\n      name:\n        other: 重复内容\n      desc:\n        other: 该问题有人问过，而且已经有了答案。\n      placeholder:\n        other: 输入已有的问题链接\n    not_a_answer:\n      name:\n        other: 不是答案\n      desc:\n        other: \"该帖是作为答案发布的，但它并没有试图回答这个问题。总之，它可能应该是个编辑、评论、另一个问题或者需要被删除。\"\n    no_longer_needed:\n      name:\n        other: 不再需要\n      desc:\n        other: 该评论已过时，对话性质或与此帖子无关。\n    something:\n      name:\n        other: 其他原因\n      desc:\n        other: 此帖子需要工作人员注意，因为是上述所列以外的其他理由。\n      placeholder:\n        other: 让我们具体知道你关心的什么\n    community_specific:\n      name:\n        other: 社区特定原因\n      desc:\n        other: 该问题不符合社区准则。\n    not_clarity:\n      name:\n        other: 需要细节或澄清\n      desc:\n        other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。\n    looks_ok:\n      name:\n        other: 看起来没问题\n      desc:\n        other: 这个帖子是好的，不是低质量。\n    needs_edit:\n      name:\n        other: 需要编辑，我已做了修改。\n      desc:\n        other: 改进和纠正你自己帖子中的问题。\n    needs_close:\n      name:\n        other: 需要关闭\n      desc:\n        other: 关闭的问题不能回答，但仍然可以编辑、投票和评论。\n    needs_delete:\n      name:\n        other: 需要删除\n      desc:\n        other: 该帖子将被删除。\n  question:\n    close:\n      duplicate:\n        name:\n          other: 垃圾信息\n        desc:\n          other: 此问题以前就有人问过，而且已经有了答案。\n      guideline:\n        name:\n          other: 社区特定原因\n        desc:\n          other: 该问题不符合社区准则。\n      multiple:\n        name:\n          other: 需要细节或澄清\n        desc:\n          other: 该问题目前涵盖多个问题。它应该只集中在一个问题上。\n      other:\n        name:\n          other: 其他原因\n        desc:\n          other: 该帖子存在上面没有列出的另一个原因。\n    operation_type:\n      asked:\n        other: 提问于\n      answered:\n        other: 回答于\n      modified:\n        other: 修改于\n    deleted_title:\n      other: 删除的问题\n    questions_title:\n      other: 问题\n  tag:\n    tags_title:\n      other: 标签\n    no_description:\n      other: 此标签没有描述。\n  notification:\n    action:\n      update_question:\n        other: 更新了问题\n      answer_the_question:\n        other: 回答了问题\n      update_answer:\n        other: 更新了答案\n      accept_answer:\n        other: 采纳了答案\n      comment_question:\n        other: 评论了问题\n      comment_answer:\n        other: 评论了答案\n      reply_to_you:\n        other: 回复了你\n      mention_you:\n        other: 提到了你\n      your_question_is_closed:\n        other: 你的问题已被关闭\n      your_question_was_deleted:\n        other: 你的问题已被删除\n      your_answer_was_deleted:\n        other: 你的答案已被删除\n      your_comment_was_deleted:\n        other: 你的评论已被删除\n      up_voted_question:\n        other: 点赞问题\n      down_voted_question:\n        other: 点踩问题\n      up_voted_answer:\n        other: 点赞答案\n      down_voted_answer:\n        other: 点踩回答\n      up_voted_comment:\n        other: 点赞评论\n      invited_you_to_answer:\n        other: 邀请你回答\n      earned_badge:\n        other: 你获得 \"{{.BadgeName}}\" 徽章\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] 确认你的新邮箱地址\"\n      body:\n        other: \"请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址：<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\n如果你没有请求此更改，请忽略此邮件。\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到<br><br>\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 回答了你的问题\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}：<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}：<br>\\n<blockquote>我想你可能知道答案。</blockquote><br>\\n<a href='{{.InviteUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}：<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] 新问题: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到 <br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] 重置密码\"\n      body:\n        other: \"有人要求在 [{{.SiteName}}] 上重置你的密码。<br><br>\\n\\n如果这不是你的操作，请安心忽略此电子邮件。<br><br>\\n\\n请点击以下链接设置一个新密码：<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n\\n如果你没有请求此更改，请忽略此邮件。\\n\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] 确认你的新账户\"\n      body:\n        other: \"欢迎加入 {{.SiteName}}！<br><br>\\n\\n请点击以下链接确认并激活你的新账户：<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\n如果上面的链接不能点击，请将其复制并粘贴到你的浏览器地址栏中。\\n<br><br>\\n\\n--<br>\\n这是系统自动发送的电子邮件，请勿回复，因为您的回复将不会被看到\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] 测试邮件\"\n      body:\n        other: \"这是测试电子邮件。\\n<br><br>\\n\\n-<br>\\n注意：这是一个自动的系统电子邮件， 请不要回复此消息，因为您的回复将不会被看到。\"\n  action_activity_type:\n    upvote:\n      other: 点赞\n    upvoted:\n      other: 点赞\n    downvote:\n      other: 点踩\n    downvoted:\n      other: 点踩\n    accept:\n      other: 采纳\n    accepted:\n      other: 已采纳\n    edit:\n      other: 编辑\n  review:\n    queued_post:\n      other: 排队的帖子\n    flagged_post:\n      other: 举报的帖子\n    suggested_post_edit:\n      other: 建议的编辑\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} 以及另外 {{ .Count }} 个...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: 自传作者\n        desc:\n          other: 填写了 <a href=\"{{ .ProfileURL }}\" target=\"_blank\">个人资料</a> 信息。\n      certified:\n        name:\n          other: 已认证\n        desc:\n          other: 完成了我们的新用户教程。\n      editor:\n        name:\n          other: 编辑者\n        desc:\n          other: 首次帖子编辑。\n      first_flag:\n        name:\n          other: 第一次举报\n        desc:\n          other: 第一次举报一个帖子\n      first_upvote:\n        name:\n          other: 第一次投票\n        desc:\n          other: 第一次投票了一个帖子。\n      first_link:\n        name:\n          other: 第一个链接\n        desc:\n          other: 第一次添加了一个链接到另一个帖子。\n      first_reaction:\n        name:\n          other: 第一个响应\n        desc:\n          other: 第一次表情回应帖子\n      first_share:\n        name:\n          other: 首次分享\n        desc:\n          other: 首次分享了一个帖子。\n      scholar:\n        name:\n          other: 学者\n        desc:\n          other: 问了一个问题并接受了一个答案。\n      commentator:\n        name:\n          other: 评论员\n        desc:\n          other: 留下5条评论。\n      new_user_of_the_month:\n        name:\n          other: 月度用户\n        desc:\n          other: 本月杰出用户\n      read_guidelines:\n        name:\n          other: 阅读指南\n        desc:\n          other: 阅读[社区准则]。\n      reader:\n        name:\n          other: 读者\n        desc:\n          other: 用10个以上的答案在主题中阅读每个答案。\n      welcome:\n        name:\n          other: 欢迎\n        desc:\n          other: 获得一个点赞投票\n      nice_share:\n        name:\n          other: 好分享\n        desc:\n          other: 分享了一个拥有25个唯一访客的帖子。\n      good_share:\n        name:\n          other: 好分享\n        desc:\n          other: 分享了一个拥有300个唯一访客的帖子。\n      great_share:\n        name:\n          other: 优秀的分享\n        desc:\n          other: 分享了一个拥有1000个唯一访客的帖子。\n      out_of_love:\n        name:\n          other: 失去爱好\n        desc:\n          other: 一天内使用了 50 个赞。\n      higher_love:\n        name:\n          other: 更高的爱好\n        desc:\n          other: 一天内使用了 50 个赞 5 次。\n      crazy_in_love:\n        name:\n          other: 爱情疯狂的\n        desc:\n          other: 一天内使用了 50 个赞 20 次。\n      promoter:\n        name:\n          other: 推荐人\n        desc:\n          other: 邀请用户。\n      campaigner:\n        name:\n          other: 宣传者\n        desc:\n          other: 邀请了3个基本用户。\n      champion:\n        name:\n          other: 冠军\n        desc:\n          other: 邀请了5个成员。\n      thank_you:\n        name:\n          other: 谢谢\n        desc:\n          other: 有 20 个赞成票的帖子，并投了 10 个赞成票。\n      gives_back:\n        name:\n          other: 返回\n        desc:\n          other: 拥有100个投票赞成的职位并放弃了100个投票。\n      empathetic:\n        name:\n          other: 情随境迁\n        desc:\n          other: 拥有500个投票赞成的职位并放弃了1000个投票。\n      enthusiast:\n        name:\n          other: 狂热\n        desc:\n          other: 连续访问10天。\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: 连续访问100天。\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: 连续访问365天。\n      anniversary:\n        name:\n          other: 周年纪念日\n        desc:\n          other: 活跃成员一年至少发布一次。\n      appreciated:\n        name:\n          other: 欣赏\n        desc:\n          other: 在 20 个帖子中获得 1个投票\n      respected:\n        name:\n          other: 尊敬\n        desc:\n          other: 100个员额获得2次补票。\n      admired:\n        name:\n          other: 仰慕\n        desc:\n          other: 300个员额获得5次补票。\n      solved:\n        name:\n          other: 已解决\n        desc:\n          other: 接受答案。\n      guidance_counsellor:\n        name:\n          other: 指导顾问\n        desc:\n          other: 接受答案。\n      know_it_all:\n        name:\n          other: 万事通\n        desc:\n          other: 接受50个答案。\n      solution_institution:\n        name:\n          other: 解决方案机构\n        desc:\n          other: 有150个答案被接受。\n      nice_answer:\n        name:\n          other: 好答案\n        desc:\n          other: 回答得分为10或以上。\n      good_answer:\n        name:\n          other: 好答案\n        desc:\n          other: 回答得分为25或更多。\n      great_answer:\n        name:\n          other: 优秀答案\n        desc:\n          other: 回答得分为50或以上。\n      nice_question:\n        name:\n          other: 好问题\n        desc:\n          other: 问题得分为10或以上。\n      good_question:\n        name:\n          other: 好问题\n        desc:\n          other: 问题得分为25或更多。\n      great_question:\n        name:\n          other: 很棒的问题\n        desc:\n          other: 问题得分为50或更多。\n      popular_question:\n        name:\n          other: 热门问题\n        desc:\n          other: 问题有 500 个浏览量。\n      notable_question:\n        name:\n          other: 值得关注问题\n        desc:\n          other: 问题有 1,000 个浏览量。\n      famous_question:\n        name:\n          other: 著名的问题\n        desc:\n          other: 问题有 5,000 个浏览量。\n      popular_link:\n        name:\n          other: 热门链接\n        desc:\n          other: 发布了一个带有50个点击的外部链接。\n      hot_link:\n        name:\n          other: 热门链接\n        desc:\n          other: 发布了一个带有300个点击的外部链接。\n      famous_link:\n        name:\n          other: 著名链接\n        desc:\n          other: 发布了一个带有100个点击的外部链接。\n    default_badge_groups:\n      getting_started:\n        name:\n          other: 完成初始化\n      community:\n        name:\n          other: Community 专题\n      posting:\n        name:\n          other: 发帖\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: 如何排版\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">引用问题或答案: <code>#4</code></p></li><li><p class=\"mb-2\">添加链接</p><pre class=\"mb-2\"><code>&lt;https://url.com&gt;<br/><br/>[标题](https://url.com)</code></pre></li><li><p class=\"mb-2\">段落之间使用空行分隔</p></li><li><p class=\"mb-2\"><em>_斜体_</em> 或者 **<strong>粗体</strong>**</p></li><li><p class=\"mb-2\">使用  个空格缩进代码</p></li><li><p class=\"mb-2\">在行首添加 <code>&gt;</code> 表示引用</p></li><li><p class=\"mb-2\">反引号进行转义 <code>`像 _这样_`</code></p></li><li><p class=\"mb-2\">使用 <code>```</code> 创建代码块</p><pre class=\"mb-0\"><code>```<br/>这是代码块<br/>```</code></pre></li></ul>\n  pagination:\n    prev: 上一页\n    next: 下一页\n  page_title:\n    question: 问题\n    questions: 问题\n    tag: 标签\n    tags: 标签\n    tag_wiki: 标签维基\n    create_tag: 创建标签\n    edit_tag: 编辑标签\n    ask_a_question: 创建问题\n    edit_question: 编辑问题\n    edit_answer: 编辑回答\n    search: 搜索\n    posts_containing: 帖子包含\n    settings: 设置\n    notifications: 通知\n    login: 登录\n    sign_up: 注册\n    account_recovery: 账号恢复\n    account_activation: 账号激活\n    confirm_email: 确认电子邮件\n    account_suspended: 账号已被封禁\n    admin: 后台管理\n    change_email: 修改邮箱\n    install: Answer 安装\n    upgrade: Answer 升级\n    maintenance: 网站维护\n    users: 用户\n    oauth_callback: 处理中\n    http_404: HTTP 错误 404\n    http_50X: HTTP 错误 500\n    http_403: HTTP 错误 403\n    logout: 退出\n    posts: 帖子\n    ai_assistant: AI 助手\n  ai_assistant:\n    description: 有问题？问它并获得答案、观点和建议。\n    recent_conversations: 新对话\n    show_more: 显示更多\n    new: 新聊天\n    ai_generate: 来自帖子的 AI，可能不准确。\n    copy: 复制\n    ask_a_follow_up: 提出后续问题\n    ask_placeholder: 提问\n  notifications:\n    title: 通知\n    inbox: 收件箱\n    achievement: 成就\n    new_alerts: 新通知\n    all_read: 全部标记为已读\n    show_more: 显示更多\n    someone: 有人\n    inbox_type:\n      all: 全部\n      posts: 帖子\n      invites: 邀请\n      votes: 投票\n    answer: 回答\n    question: 问题\n    badge_award: 徽章\n  suspended:\n    title: 你的账号账号已被封禁\n    until_time: \"你的账号被封禁直到 {{ time }}。\"\n    forever: 你的账号已被永久封禁。\n    end: 你违反了我们的社区准则。\n    contact_us: 联系我们\n  editor:\n    blockquote:\n      text: 引用\n    bold:\n      text: 粗体\n    chart:\n      text: 图表\n      flow_chart: 流程图\n      sequence_diagram: 时序图\n      class_diagram: 类图\n      state_diagram: 状态图\n      entity_relationship_diagram: 实体关系图\n      user_defined_diagram: 用户自定义图表\n      gantt_chart: 甘特图\n      pie_chart: 饼图\n    code:\n      text: 代码块\n      add_code: 添加代码块\n      form:\n        fields:\n          code:\n            label: 代码块\n            msg:\n              empty: 代码块不能为空\n          language:\n            label: 语言\n            placeholder: 自动识别\n      btn_cancel: 取消\n      btn_confirm: 添加\n    formula:\n      text: 公式\n      options:\n        inline: 行内公式\n        block: 块级公式\n    heading:\n      text: 标题\n      options:\n        h1: 标题 1\n        h2: 标题 2\n        h3: 标题 3\n        h4: 标题 4\n        h5: 标题 5\n        h6: 标题 6\n    help:\n      text: 帮助\n    hr:\n      text: 水平线\n    image:\n      text: 图片\n      add_image: 添加图片\n      tab_image: 上传图片\n      form_image:\n        fields:\n          file:\n            label: 图像文件\n            btn: 选择图片\n            msg:\n              empty: 请选择图片文件。\n              only_image: 只能上传图片文件。\n              max_size: 文件大小不能超过 {{size}} MB。\n          desc:\n            label: 描述\n      tab_url: 图片地址\n      form_url:\n        fields:\n          url:\n            label: 图片地址\n            msg:\n              empty: 图片地址不能为空\n          name:\n            label: 描述\n      btn_cancel: 取消\n      btn_confirm: 添加\n      uploading: 上传中\n    indent:\n      text: 缩进\n    outdent:\n      text: 减少缩进\n    italic:\n      text: 斜体\n    link:\n      text: 超链接\n      add_link: 添加超链接\n      form:\n        fields:\n          url:\n            label: 链接\n            msg:\n              empty: 链接不能为空。\n          name:\n            label: 描述\n      btn_cancel: 取消\n      btn_confirm: 添加\n    ordered_list:\n      text: 有序列表\n    unordered_list:\n      text: 无序列表\n    table:\n      text: 表格\n      heading: 表头\n      cell: 单元格\n    file:\n      text: 附件\n      not_supported: \"不支持的文件类型。请尝试上传其他类型的文件如： {{file_type}}。\"\n      max_size: \"上传文件超过 {{size}} MB。\"\n  close_modal:\n    title: 关闭原因是...\n    btn_cancel: 取消\n    btn_submit: 提交\n    remark:\n      empty: 不能为空。\n    msg:\n      empty: 请选择一个原因。\n  report_modal:\n    flag_title: 我举报这篇帖子的原因是...\n    close_title: 我关闭这篇帖子的原因是...\n    review_question_title: 审查问题\n    review_answer_title: 审查回答\n    review_comment_title: 审查评论\n    btn_cancel: 取消\n    btn_submit: 提交\n    remark:\n      empty: 不能为空\n    msg:\n      empty: 请选择一个原因。\n      not_a_url: URL 格式不正确。\n      url_not_match: URL 来源与当前网站不匹配。\n  tag_modal:\n    title: 创建新标签\n    form:\n      fields:\n        display_name:\n          label: 显示名称\n          msg:\n            empty: 显示名称不能为空。\n            range: 显示名称不能超过 35 个字符。\n        slug_name:\n          label: URL 固定链接\n          desc: URL 固定链接不能超过 35 个字符。\n          msg:\n            empty: URL 固定链接不能为空。\n            range: URL 固定链接不能超过 35 个字符。\n            character: URL 固定链接包含非法字符。\n        desc:\n          label: 描述\n        revision:\n          label: 编辑历史\n        edit_summary:\n          label: 编辑备注\n          placeholder: >-\n            简单描述更改原因（更正拼写、修复语法、改进格式）\n    btn_cancel: 取消\n    btn_submit: 提交\n    btn_post: 发布新标签\n  tag_info:\n    created_at: 创建于\n    edited_at: 编辑于\n    history: 历史\n    synonyms:\n      title: 同义词\n      text: 以下标签将被重置到\n      empty: 此标签目前没有同义词。\n      btn_add: 添加同义词\n      btn_edit: 编辑\n      btn_save: 保存\n    synonyms_text: 以下标签将被重置到\n    delete:\n      title: 删除标签\n      tip_with_posts: >-\n        <p>我们不允许 <strong>删除带有帖子的标签</strong>。</p> <p>请先从帖子中移除此标签。</p>\n      tip_with_synonyms: >-\n        <p>我们不允许 <strong>删除带有同义词的标签</strong>。</p> <p>请先从此标签中删除同义词。</p>\n      tip: 确定要删除吗？\n      close: 关闭\n    merge:\n      title: 合并标签\n      source_tag_title: 源标签\n      source_tag_description: 源标签及其相关数据将重新映射到目标标签。\n      target_tag_title: 目标标签\n      target_tag_description: 合并后将在这两个标签之间将创建一个同义词。\n      no_results: 没有匹配的标签\n      btn_submit: 提交\n      btn_close: 关闭\n  edit_tag:\n    title: 编辑标签\n    default_reason: 编辑标签\n    default_first_reason: 添加标签\n    btn_save_edits: 保存更改\n    btn_cancel: 取消\n  dates:\n    long_date: MM 月 DD 日\n    long_date_with_year: \"YYYY 年 MM 月 DD 日\"\n    long_date_with_time: \"YYYY 年 MM 月 DD 日 HH:mm\"\n    now: 刚刚\n    x_seconds_ago: \"{{count}} 秒前\"\n    x_minutes_ago: \"{{count}} 分钟前\"\n    x_hours_ago: \"{{count}} 小时前\"\n    hour: 小时\n    day: 天\n    hours: 小时\n    days: 日\n    month: 月\n    months: 月\n    year: 年\n  reaction:\n    heart: 爱心\n    smile: 微笑\n    frown: 愁\n    btn_label: 添加或删除回应。\n    undo_emoji: 撤销 {{ emoji }} 回应\n    react_emoji: 用 {{ emoji }} 回应\n    unreact_emoji: 撤销 {{ emoji }}\n  comment:\n    btn_add_comment: 添加评论\n    reply_to: 回复\n    btn_reply: 回复\n    btn_edit: 编辑\n    btn_delete: 删除\n    btn_flag: 举报\n    btn_save_edits: 保存更改\n    btn_cancel: 取消\n    show_more: \"{{count}} 条剩余评论\"\n    tip_question: >-\n      使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。\n    tip_answer: >-\n      使用评论对回答者进行回复，或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容，请在原问题中更改。\n    tip_vote: 它给帖子添加了一些有用的内容\n  edit_answer:\n    title: 编辑回答\n    default_reason: 编辑回答\n    default_first_reason: 添加答案\n    form:\n      fields:\n        revision:\n          label: 编辑历史\n        answer:\n          label: 回答内容\n          feedback:\n            characters: 内容长度至少 6 个字符\n        edit_summary:\n          label: 编辑摘要\n          placeholder: >-\n            简单描述更改原因（更正拼写、修复语法、改进格式）\n    btn_save_edits: 保存更改\n    btn_cancel: 取消\n  tags:\n    title: 标签\n    sort_buttons:\n      popular: 热门\n      name: 名称\n      newest: 最新\n    button_follow: 关注\n    button_following: 已关注\n    tag_label: 个问题\n    search_placeholder: 通过标签名称过滤\n    no_desc: 此标签无描述。\n    more: 更多\n    wiki: 维基\n  ask:\n    title: 创建问题\n    edit_title: 编辑问题\n    default_reason: 编辑问题\n    default_first_reason: 创建问题\n    similar_questions: 相似问题\n    form:\n      fields:\n        revision:\n          label: 修订版本\n        title:\n          label: 标题\n          placeholder: 你的主题是什么？请具体说明。\n          msg:\n            empty: 标题不能为空。\n            range: 标题最多 150 个字符\n        body:\n          label: 内容\n          msg:\n            empty: 内容不能为空。\n          hint:\n            optional_body: 描述这个问题是什么。\n            minimum_characters: \"详细描述这个问题，至少需要 {{min_content_length}} 字符。\"\n        tags:\n          label: 标签\n          msg:\n            empty: 必须选择一个标签\n        answer:\n          label: 回答内容\n          msg:\n            empty: 回答内容不能为空\n        edit_summary:\n          label: 编辑备注\n          placeholder: >-\n            简单描述更改原因（更正拼写、修复语法、改进格式）\n    btn_post_question: 提交问题\n    btn_save_edits: 保存更改\n    answer_question: 回答自己的问题\n    post_question&answer: 提交问题和回答\n  tag_selector:\n    add_btn: 添加标签\n    create_btn: 创建新标签\n    search_tag: 搜索标签\n    hint: 描述您的内容是关于什么，至少需要一个标签。\n    hint_zero_tags: 描述您的内容与什么有关。\n    hint_more_than_one_tag: \"描述您的内容是关于什么，至少需要{{min_tags_number}}个标签。\"\n    no_result: 没有匹配的标签\n    tag_required_text: 必选标签（至少一个）\n  header:\n    nav:\n      question: 问题\n      tag: 标签\n      user: 用户\n      badges: 徽章\n      profile: 用户主页\n      setting: 账号设置\n      logout: 退出\n      admin: 后台管理\n      review: 审查\n      bookmark: 收藏夹\n      moderation: 管理\n    search:\n      placeholder: 搜索\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: 更改\n    loading: 加载中...\n  pic_auth_code:\n    title: 验证码\n    placeholder: 输入图片中的文字\n    msg:\n      empty: 验证码不能为空。\n  inactive:\n    first: >-\n      就差一步！我们发送了一封激活邮件到 <bold>{{mail}}</bold>。请按照邮件中的说明激活你的账户。\n    info: \"如果没有收到，请检查你的垃圾邮件文件夹。\"\n    another: >-\n      我们向你的邮箱 <bold>{{mail}}</bold> 发送了另一封激活电子邮件。可能需要几分钟才能到达；请务必检查您的垃圾邮件箱。\n    btn_name: 重新发送激活邮件\n    change_btn_name: 更改邮箱\n    msg:\n      empty: 不能为空。\n    resend_email:\n      url_label: 确定要重新发送激活邮件吗？\n      url_text: 你也可以将上面的激活链接给该用户。\n  login:\n    login_to_continue: 登录以继续\n    info_sign: 没有账户？<1>注册</1>\n    info_login: 已经有账户？<1>登录</1>\n    agreements: 登录即表示您同意<1>隐私政策</1>和<3>服务条款</3>。\n    forgot_pass: 忘记密码?\n    name:\n      label: 名字\n      msg:\n        empty: 名字不能为空\n        range: 名称长度必须在 2 至 30 个字符之间。\n        character: '只能由 \"a-z\", \"0-9\", \" - . _\" 组成'\n    email:\n      label: 邮箱\n      msg:\n        empty: 邮箱不能为空\n    password:\n      label: 密码\n      msg:\n        empty: 密码不能为空\n        different: 两次输入密码不一致\n  account_forgot:\n    page_title: 忘记密码\n    btn_name: 发送恢复邮件\n    send_success: >-\n      如果存在邮箱为 <strong>{{mail}}</strong> 账户，你将很快收到一封重置密码的说明邮件。\n    email:\n      label: 邮箱\n      msg:\n        empty: 邮箱不能为空\n  change_email:\n    btn_cancel: 取消\n    btn_update: 更新电子邮件地址\n    send_success: >-\n      如果存在邮箱为 <strong>{{mail}}</strong> 的账户，你将很快收到一封重置密码的说明邮件。\n    email:\n      label: 新的电子邮件地址\n      msg:\n        empty: 邮箱不能为空。\n  oauth:\n    connect: 连接到 {{ auth_name }}\n    remove: 移除 {{ auth_name }}\n  oauth_bind_email:\n    subtitle: 向你的账户添加恢复邮件地址。\n    btn_update: 更新电子邮件地址\n    email:\n      label: 邮箱\n      msg:\n        empty: 邮箱不能为空。\n    modal_title: 邮箱已经存在。\n    modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗？\n    modal_cancel: 更改邮箱\n    modal_confirm: 连接到已有账户\n  password_reset:\n    page_title: 密码重置\n    btn_name: 重置我的密码\n    reset_success: >-\n      你已经成功更改密码；你将被重定向到登录页面。\n    link_invalid: >-\n      抱歉，此密码重置链接已失效。也许是你已经重置过密码了？\n    to_login: 前往登录页面\n    password:\n      label: 密码\n      msg:\n        empty: 密码不能为空。\n        length: 密码长度在8-32个字符之间\n        different: 两次输入密码不一致\n    password_confirm:\n      label: 确认新密码\n  settings:\n    page_title: 设置\n    goto_modify: 前往修改\n    nav:\n      profile: 我的资料\n      notification: 通知\n      account: 账号\n      interface: 界面\n    profile:\n      heading: 个人资料\n      btn_name: 保存\n      display_name:\n        label: 显示名称\n        msg: 昵称不能为空。\n        msg_range: 显示名称长度必须为 2-30 个字符。\n      username:\n        label: 用户名\n        caption: 用户可以通过 \"@用户名\" 来提及你。\n        msg: 用户名不能为空\n        msg_range: 显示名称长度必须为 2-30 个字符。\n        character: '只能由 \"a-z\", \"0-9\", \" - . _\" 组成'\n      avatar:\n        label: 头像\n        gravatar: Gravatar\n        gravatar_text: 你可以更改图像在\n        custom: 自定义\n        custom_text: 你可以上传你的图片。\n        default: 系统\n        msg: 请上传头像\n      bio:\n        label: 关于我\n      website:\n        label: 网站\n        placeholder: \"https://example.com\"\n        msg: 网址格式不正确\n      location:\n        label: 位置\n        placeholder: \"城市，国家\"\n    notification:\n      heading: 邮件通知\n      turn_on: 开启\n      inbox:\n        label: 收件箱通知\n        description: 你的提问有新的回答，评论，邀请回答和其他。\n      all_new_question:\n        label: 所有新问题\n        description: 获取所有新问题的通知。每周最多有50个问题。\n      all_new_question_for_following_tags:\n        label: 所有关注标签的新问题\n        description: 获取关注的标签下新问题通知。\n    account:\n      heading: 账号\n      change_email_btn: 更改邮箱\n      change_pass_btn: 更改密码\n      change_email_info: >-\n        邮件已发送。请根据指引完成验证。\n      email:\n        label: 电子邮件地址\n      new_email:\n        label: 新的电子邮件地址\n        msg: 新邮箱不能为空。\n      pass:\n        label: 当前密码\n        msg: 密码不能为空。\n      password_title: 密码\n      current_pass:\n        label: 当前密码\n        msg:\n          empty: 当前密码不能为空\n          length: 密码长度必须在 8 至 32 之间\n          different: 两次输入的密码不匹配\n      new_pass:\n        label: 新密码\n      pass_confirm:\n        label: 确认新密码\n    interface:\n      heading: 界面\n      lang:\n        label: 界面语言\n        text: 设置用户界面语言，在刷新页面后生效。\n    my_logins:\n      title: 我的登录\n      label: 使用这些账户登录或注册本网站。\n      modal_title: 移除登录\n      modal_content: 你确定要从账户里移除该登录？\n      modal_confirm_btn: 移除\n      remove_success: 移除成功\n  toast:\n    update: 更新成功\n    update_password: 密码更新成功。\n    flag_success: 感谢标记。\n    forbidden_operate_self: 禁止对自己执行操作\n    review: 您的修订将在审阅通过后显示。\n    sent_success: 发送成功\n  related_question:\n    title: 相似\n    answers: 个回答\n  linked_question:\n    title: 关联\n    description: 帖子关联到\n    no_linked_question: 没有与之关联的贴子。\n  invite_to_answer:\n    title: 受邀人\n    desc: 邀请你认为可能知道答案的人。\n    invite: 邀请回答\n    add: 添加人员\n    search: 搜索人员\n  question_detail:\n    action: 操作\n    created: 创建于\n    Asked: 提问于\n    asked: 提问于\n    update: 修改于\n    Edited: 编辑于\n    edit: 编辑于\n    commented: 评论\n    Views: 阅读次数\n    Follow: 关注此问题\n    Following: 已关注\n    follow_tip: 关注此问题以接收通知\n    answered: 回答于\n    closed_in: 关闭于\n    show_exist: 查看类似问题。\n    useful: 有用的\n    question_useful: 它是有用和明确的\n    question_un_useful: 它不明确或没用的\n    question_bookmark: 收藏该问题\n    answer_useful: 这是有用的\n    answer_un_useful: 它是没有用的\n    answers:\n      title: 个回答\n      score: 评分\n      newest: 最新\n      oldest: 最旧\n      btn_accept: 采纳\n      btn_accepted: 已被采纳\n    write_answer:\n      title: 你的回答\n      edit_answer: 编辑我的回答\n      btn_name: 提交你的回答\n      add_another_answer: 添加另一个回答\n      confirm_title: 继续回答\n      continue: 继续\n      confirm_info: >-\n        <p>你确定要提交一个新的回答吗？</p><p>作为替代，你可以通过编辑来完善和改进之前的回答。</p>\n      empty: 回答内容不能为空。\n      characters: 内容长度至少 6 个字符。\n      tips:\n        header_1: 感谢你的回答\n        li1_1: 请务必确定在 <strong>回答问题</strong>。提供详细信息并分享你的研究。\n        li1_2: 用参考资料或个人经历来支持你所做的任何陈述。\n        header_2: 但是 <strong>请避免</strong>...\n        li2_1: 请求帮助，寻求澄清，或答复其他答案。\n    reopen:\n      confirm_btn: 重新打开\n      title: 重新打开这个帖子\n      content: 确定要重新打开吗？\n    list:\n      confirm_btn: 列表显示\n      title: 列表中显示这个帖子\n      content: 确定要列表中显示这个帖子吗？\n    unlist:\n      confirm_btn: 列表隐藏\n      title: 从列表中隐藏这个帖子\n      content: 确定要从列表中隐藏这个帖子吗？\n    pin:\n      title: 置顶该帖子\n      content: 你确定要全局置顶吗？这个帖子将出现在所有帖子列表的顶部。\n      confirm_btn: 置顶\n  delete:\n    title: 删除\n    question: >-\n      我们不建议 <strong>删除有回答的帖子</strong>。因为这样做会使得后来的读者无法从该帖子中获得帮助。</p><p>如果删除过多有回答的帖子，你的账号将会被禁止提问。你确定要删除吗？\n    answer_accepted: >-\n      <p>我们不建议<strong>删除被采纳的回答</strong>。因为这样做会使得后来的读者无法从该帖子中获得帮助。</p>如果删除过多被采纳的回答，你的账号将会被禁止回答任何提问。你确定要删除吗？\n    other: 你确定要删除？\n    tip_answer_deleted: 该回答已被删除\n    undelete_title: 撤销删除本帖\n    undelete_desc: 你确定你要撤销删除吗？\n  btns:\n    confirm: 确认\n    cancel: 取消\n    edit: 编辑\n    save: 保存\n    delete: 删除\n    undelete: 撤消删除\n    list: 列表显示\n    unlist: 列表隐藏\n    unlisted: 已隐藏\n    login: 登录\n    signup: 注册\n    logout: 退出\n    verify: 验证\n    create: 创建\n    approve: 批准\n    reject: 拒绝\n    skip: 跳过\n    discard_draft: 丢弃草稿\n    pinned: 已置顶\n    all: 全部\n    question: 问题\n    answer: 回答\n    comment: 评论\n    refresh: 刷新\n    resend: 重新发送\n    deactivate: 取消激活\n    active: 激活\n    suspend: 封禁\n    unsuspend: 解禁\n    close: 关闭\n    reopen: 重新打开\n    ok: 确定\n    light: 浅色\n    dark: 深色\n    system_setting: 跟随系统\n    default: 默认\n    reset: 重置\n    tag: 标签\n    post_lowercase: 帖子\n    filter: 筛选\n    ignore: 忽略\n    submit: 提交\n    normal: 正常\n    closed: 已关闭\n    deleted: 已删除\n    deleted_permanently: 永久删除\n    pending: 等待处理\n    more: 更多\n    view: 浏览量\n    card: 卡片\n    compact: 紧凑\n    display_below: 在下方显示\n    always_display: 总是显示\n    or: 或者\n    back_sites: 返回网站\n  search:\n    title: 搜索结果\n    keywords: 关键词\n    options: 选项\n    follow: 关注\n    following: 已关注\n    counts: \"{{count}} 个结果\"\n    counts_loading: \"... 个结果\"\n    more: 更多\n    sort_btns:\n      relevance: 相关性\n      newest: 最新的\n      active: 活跃的\n      score: 评分\n      more: 更多\n    tips:\n      title: 高级搜索提示\n      tag: \"<1>[tag]</1> 在指定标签中搜索\"\n      user: \"<1>user:username</1> 根据作者搜索\"\n      answer: \"<1>answers:0</1> 搜索未回答的问题\"\n      score: \"<1>score:3</1> 评分 3+ 的帖子\"\n      question: \"<1>is:question</1> 搜索问题\"\n      is_answer: \"<1>is:answer</1> 搜索回答\"\n    empty: 找不到任何相关的内容。<br /> 请尝试其他关键字，或者减少查找内容的长度。\n  share:\n    name: 分享\n    copy: 复制链接\n    via: 分享到...\n    copied: 已复制\n    facebook: 分享到 Facebook\n    twitter: 分享到 X\n  cannot_vote_for_self: 你不能给自己的帖子投票。\n  modal_confirm:\n    title: 发生错误...\n  delete_permanently:\n    title: 永久删除\n    content: 您确定要永久删除吗？\n  account_result:\n    success: 你的账号已通过验证，即将返回首页。\n    link: 返回首页\n    oops: 糟糕！\n    invalid: 您使用的链接不再有效。\n    confirm_new_email: 你的电子邮箱已更新\n    confirm_new_email_invalid: >-\n      抱歉，此验证链接已失效。也许是你的邮箱已经成功更改了？\n  unsubscribe:\n    page_title: 退订\n    success_title: 退订成功\n    success_desc: 您已成功退订，并且将不会再收到我们的邮件。\n    link: 更改设置\n  question:\n    following_tags: 已关注的标签\n    edit: 编辑\n    save: 保存\n    follow_tag_tip: 关注标签来筛选你的问题列表。\n    hot_questions: 热门问题\n    all_questions: 全部问题\n    x_questions: \"{{ count }} 个问题\"\n    x_answers: \"{{ count }} 个回答\"\n    x_posts: \"{{ count }} 个帖子\"\n    questions: 问题\n    answers: 回答\n    newest: 最新\n    active: 活跃\n    hot: 热门\n    frequent: 频繁的\n    recommend: 推荐\n    score: 评分\n    unanswered: 未回答\n    modified: 更新于\n    answered: 回答于\n    asked: 提问于\n    closed: 已关闭\n    follow_a_tag: 关注一个标签\n    more: 更多\n  personal:\n    overview: 概览\n    answers: 回答\n    answer: 回答\n    questions: 问题\n    question: 问题\n    bookmarks: 收藏\n    reputation: 声望\n    comments: 评论\n    votes: 得票\n    badges: 徽章\n    newest: 最新\n    score: 评分\n    edit_profile: 编辑资料\n    visited_x_days: \"已访问 {{ count }} 天\"\n    viewed: 浏览次数\n    joined: 加入于\n    comma: \"，\"\n    last_login: 上次登录\n    about_me: 关于我\n    about_me_empty: \"// Hello, World!\"\n    top_answers: 高分回答\n    top_questions: 高分问题\n    stats: 状态\n    list_empty: 没有找到相关的内容。<br />试试看其他选项卡？\n    content_empty: 未找到帖子。\n    accepted: 已采纳\n    answered: 回答于\n    asked: 提问于\n    downvoted: 点踩\n    mod_short: 版主\n    mod_long: 版主\n    x_reputation: 声望\n    x_votes: 得票\n    x_answers: 个回答\n    x_questions: 个问题\n    recent_badges: 最近的徽章\n  install:\n    title: 安装\n    next: 下一步\n    done: 完成\n    config_yaml_error: 无法创建 config.yaml 文件。\n    lang:\n      label: 请选择一种语言\n    db_type:\n      label: 数据库引擎\n    db_username:\n      label: 用户名\n      placeholder: root\n      msg: 用户名不能为空\n    db_password:\n      label: 密码\n      placeholder: root\n      msg: 密码不能为空\n    db_host:\n      label: 数据库主机\n      placeholder: \"db:3306\"\n      msg: 数据库地址不能为空\n    db_name:\n      label: 数据库名\n      placeholder: 回答\n      msg: 数据库名称不能为空。\n    db_file:\n      label: 数据库文件\n      placeholder: /data/answer.db\n      msg: 数据库文件不能为空。\n    ssl_enabled:\n      label: 启用 SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL 模式\n    ssl_root_cert:\n      placeholder: sslrootcert文件路径\n      msg: sslrootcert 文件的路径不能为空\n    ssl_cert:\n      placeholder: sslcert文件路径\n      msg: sslcert 文件的路径不能为空\n    ssl_key:\n      placeholder: sslkey 文件路径\n      msg: sslcert 文件的路径不能为空\n    config_yaml:\n      title: 创建 config.yaml\n      label: 已创建 config.yaml 文件。\n      desc: >-\n        你可以手动在 <1>/var/wwww/xxx/</1> 目录中创建 <1>config.yaml</1> 文件并粘贴以下文本。\n      info: 完成后，点击“下一步”按钮。\n    site_information: 站点信息\n    admin_account: 管理员账号\n    site_name:\n      label: 站点名称\n      msg: 站点名称不能为空。\n      msg_max_length: 站点名称长度不得超过 30 个字符。\n    site_url:\n      label: 网站网址\n      text: 此网站的网址。\n      msg:\n        empty: 网址不能为空。\n        incorrect: 网址格式不正确。\n        max_length: 网址长度不得超过 512 个字符。\n    contact_email:\n      label: 联系邮箱\n      text: 负责本网站的主要联系人的电子邮件地址。\n      msg:\n        empty: 联系人邮箱不能为空。\n        incorrect: 联系人邮箱地址不正确。\n    login_required:\n      label: 私有的\n      switch: 需要登录\n      text: 只有登录用户才能访问这个社区。\n    admin_name:\n      label: 名字\n      msg: 名字不能为空。\n      character: '只能由 \"a-z\", \"0-9\", \" - . _\" 组成'\n      msg_max_length: 名称长度必须在 2 至 30 个字符之间。\n    admin_password:\n      label: 密码\n      text: >-\n        您需要此密码才能登录。请将其存储在一个安全的位置。\n      msg: 密码不能为空。\n      msg_min_length: 密码必须至少 8 个字符长。\n      msg_max_length: 密码长度不能超过 32 个字符。\n    admin_confirm_password:\n      label: \"确认密码\"\n      text: \"请重新输入您的密码以确认。\"\n      msg: \"确认密码不一致。\"\n    admin_email:\n      label: 邮箱\n      text: 您需要此电子邮件才能登录。\n      msg:\n        empty: 邮箱不能为空。\n        incorrect: 邮箱格式不正确。\n    ready_title: 您的网站已准备好\n    ready_desc: >-\n      如果你想改变更多的设置，请访问 <1>管理区域</1>；在网站菜单中找到它。\n    good_luck: \"玩得愉快，祝你好运！\"\n    warn_title: 警告\n    warn_desc: >-\n      文件 <1>config.yaml</1> 已存在。如果你要重置该文件中的任何配置项，请先删除它。\n    install_now: 您可以尝试 <1>现在安装</1>。\n    installed: 已安裝\n    installed_desc: >-\n      你似乎已经安装过了。如果要重新安装，请先清除旧的数据库表。\n    db_failed: 数据连接异常！\n    db_failed_desc: >-\n      这或者意味着数据库信息在 <1>config.yaml</1> 文件不正确，或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。\n  counts:\n    views: 次浏览\n    votes: 个点赞\n    answers: 个回答\n    accepted: 已被采纳\n  page_error:\n    http_error: HTTP 错误 {{ code }}\n    desc_403: 您无权访问此页面。\n    desc_404: 很抱歉，此页面不存在。\n    desc_50X: 服务器遇到了一个错误，无法完成你的请求。\n    back_home: 返回首页\n  page_maintenance:\n    desc: \"我们正在进行维护，我们将很快回来。\"\n  nav_menus:\n    dashboard: 后台管理\n    contents: 内容管理\n    questions: 问题\n    answers: 回答\n    users: 用户管理\n    badges: 徽章\n    flags: 举报管理\n    settings: 站点设置\n    general: 一般\n    interface: 界面\n    smtp: SMTP\n    branding: 品牌\n    legal: 法律条款\n    write: 撰写\n    terms: 服务条款\n    tos: 服务条款\n    privacy: 隐私政策\n    seo: SEO\n    customize: 自定义\n    themes: 主题\n    login: 登录\n    privileges: 特权\n    plugins: 插件\n    installed_plugins: 已安装插件\n    apperance: 外观\n    community: 社区\n    advanced: 高级选项\n    tags: 标签\n    rules: 规则\n    policies: 政策\n    security: 安全\n    files: 文件\n    apikeys: API 密钥\n    intelligence: 智力\n    ai_assistant: AI 助手\n    ai_settings: AI 设置\n    mcp: MCP\n  website_welcome: 欢迎来到 {{site_name}}\n  user_center:\n    login: 登录\n    qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。\n    login_failed_email_tip: 登录失败，请允许此应用访问您的邮箱信息，然后重试。\n  badges:\n    modal:\n      title: 恭喜\n      content: 你赢得了一个新徽章。\n      close: 关闭\n      confirm: 查看徽章\n    title: 徽章\n    awarded: 授予\n    earned_×: 以获得 ×{{ number }}\n    ×_awarded: \"{{ number }} 得到\"\n    can_earn_multiple: 你可以多次获得\n    earned: 获得\n  admin:\n    admin_header:\n      title: 后台管理\n    dashboard:\n      title: 后台管理\n      welcome: 欢迎来到管理后台！\n      site_statistics: 站点统计\n      questions: \"问题:\"\n      resolved: \"已解决:\"\n      unanswered: \"未回答:\"\n      answers: \"回答:\"\n      comments: \"评论:\"\n      votes: \"投票:\"\n      users: \"用户:\"\n      flags: \"举报:\"\n      reviews: \"审查:\"\n      site_health: 网站健康\n      version: \"版本\"\n      https: \"HTTPS:\"\n      upload_folder: \"上传文件夹：\"\n      run_mode: \"运行模式：\"\n      private: 私有\n      public: 公开\n      smtp: \"SMTP:\"\n      timezone: \"时区：\"\n      system_info: 系统信息\n      go_version: \"Go版本：\"\n      database: \"数据库：\"\n      database_size: \"数据库大小：\"\n      storage_used: \"已用存储空间：\"\n      uptime: \"运行时间：\"\n      links: 链接\n      plugins: 插件\n      github: GitHub\n      blog: 博客\n      contact: 联系\n      forum: 论坛\n      documents: 文档\n      feedback: 用户反馈\n      support: 帮助\n      review: 审查\n      config: 配置\n      update_to: 更新到\n      latest: 最新版本\n      check_failed: 校验失败\n      \"yes\": \"是\"\n      \"no\": \"否\"\n      not_allowed: 拒绝\n      allowed: 允许\n      enabled: 已启用\n      disabled: 停用\n      writable: 可写\n      not_writable: 不可写\n    flags:\n      title: 举报\n      pending: 等待处理\n      completed: 已完成\n      flagged: 被举报内容\n      flagged_type: 标记了 {{ type }}\n      created: 创建于\n      action: 操作\n      review: 审查\n    user_role_modal:\n      title: 更改用户状态为...\n      btn_cancel: 取消\n      btn_submit: 提交\n    new_password_modal:\n      title: 设置新密码\n      form:\n        fields:\n          password:\n            label: 密码\n            text: 用户将被退出，需要再次登录。\n            msg: 密码的长度必须是8-32个字符。\n      btn_cancel: 取消\n      btn_submit: 提交\n    edit_profile_modal:\n      title: 编辑资料\n      form:\n        fields:\n          display_name:\n            label: 显示名称\n            msg_range: 显示名称长度必须为 2-30 个字符。\n          username:\n            label: 用户名\n            msg_range: 用户名长度必须为 2-30 个字符。\n          email:\n            label: 电子邮件地址\n            msg_invalid: 无效的邮箱地址\n      edit_success: 修改成功\n      btn_cancel: 取消\n      btn_submit: 提交\n    user_modal:\n      title: 添加新用户\n      form:\n        fields:\n          users:\n            label: 批量添加用户\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: 用逗号分隔“name, email, password”，每行一个用户。\n            msg: \"请输入用户的邮箱，每行一个。\"\n          display_name:\n            label: 显示名称\n            msg: 显示名称长度必须为 2-30 个字符\n          email:\n            label: 邮箱\n            msg: 邮箱无效。\n          password:\n            label: 密码\n            msg: 密码的长度必须是8-32个字符。\n      btn_cancel: 取消\n      btn_submit: 提交\n    users:\n      title: 用户\n      name: 名称\n      email: 邮箱\n      reputation: 声望\n      created_at: 创建时间\n      delete_at: 删除时间\n      suspend_at: 封禁时间\n      suspend_until: 封禁到期\n      status: 状态\n      role: 角色\n      action: 操作\n      change: 更改\n      all: 全部\n      staff: 工作人员\n      more: 更多\n      inactive: 不活跃\n      suspended: 已封禁\n      deleted: 已删除\n      normal: 正常\n      Moderator: 版主\n      Admin: 管理员\n      User: 用户\n      filter:\n        placeholder: \"按名称筛选，用户：id\"\n      set_new_password: 设置新密码\n      edit_profile: 编辑资料\n      change_status: 更改状态\n      change_role: 更改角色\n      show_logs: 显示日志\n      add_user: 添加用户\n      deactivate_user:\n        title: 停用用户\n        content: 未激活的用户必须重新验证他们的邮箱。\n      delete_user:\n        title: 删除此用户\n        content: 确定要删除此用户？此操作无法撤销！\n        remove: 移除内容\n        label: 删除所有问题、 答案、 评论等\n        text: 如果你只想删除用户账户，请不要选中此项。\n      suspend_user:\n        title: 挂起此用户\n        content: 被封禁的用户将无法登录。\n        label: 用户将被封禁多长时间？\n        forever: 永久\n    questions:\n      page_title: 问题\n      unlisted: 已隐藏\n      post: 标题\n      votes: 得票数\n      answers: 回答数\n      created: 创建于\n      status: 状态\n      action: 操作\n      change: 更改\n      pending: 等待处理\n      filter:\n        placeholder: \"按标题过滤，问题:id\"\n    answers:\n      page_title: 回答\n      post: 标题\n      votes: 得票数\n      created: 创建于\n      status: 状态\n      action: 操作\n      change: 更改\n      filter:\n        placeholder: \"按标题筛选，答案:id\"\n    general:\n      page_title: 一般\n      name:\n        label: 站点名称\n        msg: 不能为空\n        text: \"站点的名称，作为站点的标题。\"\n      site_url:\n        label: 网站网址\n        msg: 网站网址不能为空。\n        validate: 请输入一个有效的 URL。\n        text: 此网站的地址。\n      short_desc:\n        label: 简短站点描述\n        msg: 简短网站描述不能为空。\n        text: \"简短的标语，作为网站主页的标题（Html 的 title 标签）。\"\n      desc:\n        label: 站点描述\n        msg: 网站描述不能为空。\n        text: \"使用一句话描述本站，作为网站的描述（Html 的 meta 标签）。\"\n      contact_email:\n        label: 联系邮箱\n        msg: 联系人邮箱不能为空。\n        validate: 联系人邮箱无效。\n        text: 本网站的主要联系邮箱地址。\n      check_update:\n        label: 软件更新\n        text: 自动检查软件更新\n    interface:\n      page_title: 界面\n      language:\n        label: 界面语言\n        msg: 不能为空\n        text: 设置用户界面语言，在刷新页面后生效。\n      time_zone:\n        label: 时区\n        msg: 时区不能为空。\n        text: 选择一个与您相同时区的城市。\n      avatar:\n        label: 默认头像\n        text: 没有自定义头像的用户。\n      gravatar_base_url:\n        label: Gravatar 根路径 URL\n        text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: 发件人邮箱\n        msg: 发件人邮箱不能为空。\n        text: 用于发送邮件的地址。\n      from_name:\n        label: 发件人\n        msg: 不能为空\n        text: 发件人的名字。\n      smtp_host:\n        label: SMTP 主机\n        msg: 不能为空\n        text: 邮件服务器\n      encryption:\n        label: 加密\n        msg: 不能为空\n        text: 对于大多数服务器而言，SSL 是推荐开启的。\n        ssl: SSL\n        tls: TLS\n        none: 无加密\n      smtp_port:\n        label: SMTP 端口\n        msg: SMTP 端口必须在 1 ~ 65535 之间。\n        text: 邮件服务器的端口号。\n      smtp_username:\n        label: SMTP 用户名\n        msg: 不能为空\n      smtp_password:\n        label: SMTP 密码\n        msg: 不能为空\n      test_email_recipient:\n        label: 测试收件邮箱\n        text: 提供用于接收测试邮件的邮箱地址。\n        msg: 测试收件邮箱无效\n      smtp_authentication:\n        label: 启用身份验证\n        title: SMTP 身份验证\n        msg: 不能为空\n        \"yes\": \"是\"\n        \"no\": \"否\"\n    branding:\n      page_title: 品牌\n      logo:\n        label: 网站标志（Logo）\n        msg: 图标不能为空。\n        text: 在你的网站左上方的Logo图标。使用一个高度为56，长宽比大于3:1的宽长方形图像。如果留空，将显示网站标题文本。\n      mobile_logo:\n        label: 移动端 Logo\n        text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空，将使用 \"Logo\"设置中的图像。\n      square_icon:\n        label: 方形图标\n        msg: 方形图标不能为空。\n        text: 用作元数据图标的基础的图像。最好是大于512x512。\n      favicon:\n        label: 收藏夹图标\n        text: 网站的图标。要在 CDN 正常工作，它必须是 png。 将调整大小到32x32。如果留空，将使用“方形图标”。\n    legal:\n      page_title: 法律条款\n      terms_of_service:\n        label: 服务条款\n        text: \"您可以在此添加服务内容的条款。如果您已经在别处托管了文档，请在这里提供完整的URL。\"\n      privacy_policy:\n        label: 隐私政策\n        text: \"您可以在此添加隐私政策内容。如果您已经在别处托管了文档，请在这里提供完整的URL。\"\n      external_content_display:\n        label: 外部内容\n        text: \"内容包括从外部网站嵌入的图像、视频和媒体。\"\n        always_display: 总是显示外部内容\n        ask_before_display: 在显示外部内容之前询问\n    write:\n      page_title: 文件\n      min_content:\n        label: 最小问题长度\n        text: 最小允许的问题内容长度（字符）。\n      restrict_answer:\n        title: 回答编辑\n        label: 每个用户对于每个问题只能有一个回答\n        text: \"用户可以使用编辑按钮优化已有的回答\"\n      min_tags:\n        label: \"问题的最少标签数\"\n        text: \"一个问题所需标签的最小数量。\"\n      recommend_tags:\n        label: 推荐标签\n        text: \"推荐标签将默认显示在下拉列表中。\"\n        msg:\n          contain_reserved: \"推荐标签不能包含保留标签\"\n      required_tag:\n        title: 设置必填标签\n        label: 设置“推荐标签”为必需的标签\n        text: \"每个新问题必须至少有一个推荐标签。\"\n      reserved_tags:\n        label: 保留标签\n        text: \"只有版主才能使用保留的标签。\"\n      image_size:\n        label: 最大图像大小 (MB)\n        text: \"最大图像上传大小.\"\n      attachment_size:\n        label: 最大附件大小 (MB)\n        text: \"最大附件文件上传大小。\"\n      image_megapixels:\n        label: 最大图像兆像素\n        text: \"允许图像的最大兆位数。\"\n      image_extensions:\n        label: 允许的图像后缀\n        text: \"允许图像显示的文件扩展名的列表，用英文逗号分隔。\"\n      attachment_extensions:\n        label: 允许的附件后缀\n        text: \"允许上传的文件扩展名列表与英文逗号分开。警告：允许上传可能会导致安全问题。\"\n    seo:\n      page_title: 搜索引擎优化\n      permalink:\n        label: 固定链接\n        text: 自定义URL结构可以提高可用性，以及你的链接的向前兼容性。\n      robots:\n        label: robots.txt\n        text: 这将永久覆盖任何相关的网站设置。\n    themes:\n      page_title: 主题\n      themes:\n        label: 主题\n        text: 选择一个现有主题。\n      color_scheme:\n        label: 配色方案\n      navbar_style:\n        label: 导航栏背景样式\n      primary_color:\n        label: 主色调\n        text: 修改您主题使用的颜色\n      layout:\n        label: 布局\n        full_width: 全宽度\n        fixed_width: 固定宽度\n    css_and_html:\n      page_title: CSS 与 HTML\n      custom_css:\n        label: 自定义 CSS\n        text: >\n\n      head:\n        label: 头部\n        text: >\n\n      header:\n        label: 页眉\n        text: >\n\n      footer:\n        label: 页脚\n        text: 这将在 &lt;/body> 之前插入。\n      sidebar:\n        label: 侧边栏\n        text: 这将插入侧边栏中。\n    login:\n      page_title: 登录\n      membership:\n        title: 会员\n        label: 允许新注册\n        text: 关闭以防止任何人创建新账户。\n      email_registration:\n        title: 邮箱注册\n        label: 允许邮箱注册\n        text: 关闭以阻止任何人通过邮箱创建新账户。\n      allowed_email_domains:\n        title: 允许的邮箱域\n        text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。\n      private:\n        title: 非公开的\n        label: 需要登录\n        text: 只有登录用户才能访问这个社区。\n      password_login:\n        title: 密码登录\n        label: 允许使用邮箱和密码登录\n        text: \"警告：如果您未配置过其他登录方式，关闭密码登录后您则可能无法登录。\"\n    installed_plugins:\n      title: 已安装插件\n      plugin_link: 插件扩展功能。您可以在<1>插件仓库</1>中找到插件。\n      filter:\n        all: 全部\n        active: 已启用\n        inactive: 未启用\n        outdated: 已过期\n      plugins:\n        label: 插件\n        text: 选择一个现有的插件。\n      name: 名称\n      version: 版本\n      status: 状态\n      action: 操作\n      deactivate: 停用\n      activate: 启用\n      settings: 设置\n    settings_users:\n      title: 用户\n      avatar:\n        label: 默认头像\n        text: 没有自定义头像的用户。\n      gravatar_base_url:\n        label: Gravatar 根路径 URL\n        text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。\n      profile_editable:\n        title: 个人资料可编辑\n      allow_update_display_name:\n        label: 允许用户修改显示名称\n      allow_update_username:\n        label: 允许用户修改用户名\n      allow_update_avatar:\n        label: 允许用户修改个人头像\n      allow_update_bio:\n        label: 允许用户修改个人介绍\n      allow_update_website:\n        label: 允许用户修改个人主页网址\n      allow_update_location:\n        label: 允许用户更改位置\n    privilege:\n      title: 特权\n      level:\n        label: 级别所需声望\n        text: 选择特权所需的声望值\n      msg:\n        should_be_number: 输入必须是数字\n        number_larger_1: 数字应该大于等于 1\n    badges:\n      action: 操作\n      active: 活跃的\n      activate: 启用\n      all: 全部\n      awards: 奖项\n      deactivate: 取消激活\n      filter:\n        placeholder: 按名称筛选，或使用 badge:id\n      group: 组\n      inactive: 未启用\n      name: 名字\n      show_logs: 显示日志\n      status: 状态\n      title: 徽章\n    apikeys:\n      title: API 密钥\n      add_api_key: 添加 API 密钥\n      desc: 描述\n      scope: 范围\n      key: 密钥\n      created: 创建于\n      last_used: 最后使用\n      add_or_edit_modal:\n        add_title: 添加 API 密钥\n        edit_title: 编辑 API 密钥\n        description: 描述\n        description_required: 请输入描述。\n        scope: 范围\n        global: 全局\n        read-only: 只读\n      created_modal:\n        title: 已创建API密钥\n        api_key: API 密钥\n        description: 此密钥将不会再次显示。请确保您在继续之前拿到一份副本。\n      delete_modal:\n        title: 删除API密钥\n        content: 任何使用此密钥的应用程序或脚本都将无法访问API。这是永久性的！\n    ai_settings:\n      enabled:\n        label: AI 已启用\n        check: 启用AI功能\n        text: AI 模型必须正确配置才能使用。\n      provider:\n        label: 提供商\n      api_host:\n        label: API 主机\n        msg: API 主机是必需的\n      api_key:\n        label: API 密钥\n        check: 检查\n        check_success: \"连接成功。\"\n        msg: API 密钥是必填项\n      model:\n        label: 模型\n        msg: 模型是必需的\n      add_success: AI 设置更新成功。\n    conversations:\n      topic: 主题\n      helpful: 有帮助\n      unhelpful: 没有帮助\n      created: 创建于\n      action: 操作\n      empty: 没有找到会话\n      delete_modal:\n        title: 删除对话\n        content: 您确定要删除此对话吗？这是永久性的！\n        delete_success: 对话删除成功。\n    mcp:\n      mcp_server:\n        label: MCP服务器\n        switch: 已启用\n      type:\n        label: 类型\n      url:\n        label: 链接\n      http_header:\n        label: HTTP Header\n        text: 请将 {key} 替换为 API 密钥。\n  form:\n    optional: (选填)\n    empty: 不能为空\n    invalid: 是无效的\n    btn_submit: 保存\n    not_found_props: \"所需属性 {{ key }} 未找到。\"\n    select: 选择\n  page_review:\n    review: 评论\n    proposed: 提案\n    question_edit: 问题编辑\n    answer_edit: 回答编辑\n    tag_edit: '标签管理: 编辑标签'\n    edit_summary: 编辑备注\n    edit_question: 编辑问题\n    edit_answer: 编辑回答\n    edit_tag: 编辑标签\n    empty: 没有剩余的审核任务。\n    approve_revision_tip: 您是否批准此修订？\n    approve_flag_tip: 您是否批准此举报？\n    approve_post_tip: 您是否批准此帖子？\n    approve_user_tip: 您是否批准此修订？\n    suggest_edits: 建议的编辑\n    flag_post: 举报帖子\n    flag_user: 举报用户\n    queued_post: 排队的帖子\n    queued_user: 排队用户\n    filter_label: 类型\n    reputation: 声望值\n    flag_post_type: 举报这个帖子的类型是 {{ type }}\n    flag_user_type: 举报这个用户的类型是 {{ type }}\n    edit_post: 编辑帖子\n    list_post: 文章列表\n    unlist_post: 隐藏的帖子\n  timeline:\n    undeleted: 取消删除\n    deleted: 删除\n    downvote: 反对\n    upvote: 点赞\n    accept: 采纳\n    cancelled: 已取消\n    commented: '评论:'\n    rollback: 回滚\n    edited: 最后编辑于\n    answered: 回答于\n    asked: 提问于\n    closed: 关闭\n    reopened: 重新开启\n    created: 创建于\n    pin: 已置顶\n    unpin: 取消置頂\n    show: 已显示\n    hide: 已隐藏\n    title: \"历史记录\"\n    tag_title: \"时间线\"\n    show_votes: \"显示投票\"\n    n_or_a: N/A\n    title_for_question: \"时间线\"\n    title_for_answer: \"{{ title }} 的 {{ author }} 回答时间线\"\n    title_for_tag: \"时间线\"\n    datetime: 日期时间\n    type: 类型\n    by: 由\n    comment: 评论\n    no_data: \"空空如也\"\n  users:\n    title: 用户\n    users_with_the_most_reputation: 本周声望最高的用户\n    users_with_the_most_vote: 本周投票最多的用户\n    staffs: 我们的社区工作人员\n    reputation: 声望值\n    votes: 投票\n  prompt:\n    leave_page: 确定要离开此页面？\n    changes_not_save: 您的更改尚未保存\n  draft:\n    discard_confirm: 您确定要丢弃您的草稿吗？\n  messages:\n    post_deleted: 该帖子已被删除。\n    post_cancel_deleted: 此帖子已被删除\n    post_pin: 该帖子已被置顶。\n    post_unpin: 该帖子已被取消置顶。\n    post_hide_list: 此帖子已经从列表中隐藏。\n    post_show_list: 该帖子已显示到列表中。\n    post_reopen: 这个帖子已被重新打开.\n    post_list: 这个帖子已经被显示\n    post_unlist: 这个帖子已经被隐藏\n    post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。\n    post_closed: 此帖已关闭。\n    answer_deleted: 该回答已被删除.\n    answer_cancel_deleted: 此答案已取消删除。\n    change_user_role: 此用户的角色已被更改。\n    user_inactive: 此用户已经处于未激活状态。\n    user_normal: 此用户已经是正常的。\n    user_suspended: 此用户已被封禁。\n    user_deleted: 此用户已被删除\n    user_added: 用户添加成功。\n    badge_activated: 此徽章已被激活。\n    badge_inactivated: 此徽章已被禁用。\n    users_deleted: 这些用户已被删除。\n    posts_deleted: 这些问题已被删除。\n    answers_deleted: 这些答案已被删除。\n    copy: 复制到剪贴板\n    copied: 已复制\n    external_content_warning: 外部图像/媒体未显示。\n\n\n"
  },
  {
    "path": "i18n/zh_TW.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# The following fields are used for back-end\nbackend:\n  base:\n    success:\n      other: 成功。\n    unknown:\n      other: 未知的錯誤。\n    request_format_error:\n      other: 要求格式錯誤。\n    unauthorized_error:\n      other: 未授權。\n    database_error:\n      other: 資料伺服器錯誤。\n    forbidden_error:\n      other: 已拒絕存取。\n    duplicate_request_error:\n      other: 重複送出。\n  action:\n    report:\n      other: 檢舉\n    edit:\n      other: 編輯\n    delete:\n      other: 删除\n    close:\n      other: 關閉\n    reopen:\n      other: 再次開啟。\n    forbidden_error:\n      other: 已拒絕存取。\n    pin:\n      other: 置頂\n    hide:\n      other: 不公開\n    unpin:\n      other: 取消置頂\n    show:\n      other: 清單\n    invite_someone_to_answer:\n      other: 編輯\n    undelete:\n      other: 還原\n    merge:\n      other: 合併\n  role:\n    name:\n      user:\n        other: 使用者\n      admin:\n        other: 管理員\n      moderator:\n        other: 版主\n    description:\n      user:\n        other: 預設沒有特別閱讀權限。\n      admin:\n        other: 擁有存取此網站的全部權限。\n      moderator:\n        other: 可以存取除了管理員設定以外的所有貼文。\n  privilege:\n    level_1:\n      description:\n        other: Level 1 (less reputation required for private team, group)\n    level_2:\n      description:\n        other: Level 2 (low reputation required for startup community)\n    level_3:\n      description:\n        other: Level 3 (high reputation required for mature community)\n    level_custom:\n      description:\n        other: Custom Level\n    rank_question_add_label:\n      other: Ask question\n    rank_answer_add_label:\n      other: Write answer\n    rank_comment_add_label:\n      other: 寫留言\n    rank_report_add_label:\n      other: Flag\n    rank_comment_vote_up_label:\n      other: Upvote comment\n    rank_link_url_limit_label:\n      other: Post more than 2 links at a time\n    rank_question_vote_up_label:\n      other: Upvote question\n    rank_answer_vote_up_label:\n      other: Upvote answer\n    rank_question_vote_down_label:\n      other: Downvote question\n    rank_answer_vote_down_label:\n      other: Downvote answer\n    rank_invite_someone_to_answer_label:\n      other: Invite someone to answer\n    rank_tag_add_label:\n      other: Create new tag\n    rank_tag_edit_label:\n      other: Edit tag description (need to review)\n    rank_question_edit_label:\n      other: Edit other's question (need to review)\n    rank_answer_edit_label:\n      other: Edit other's answer (need to review)\n    rank_question_edit_without_review_label:\n      other: Edit other's question without review\n    rank_answer_edit_without_review_label:\n      other: Edit other's answer without review\n    rank_question_audit_label:\n      other: Review question edits\n    rank_answer_audit_label:\n      other: Review answer edits\n    rank_tag_audit_label:\n      other: Review tag edits\n    rank_tag_edit_without_review_label:\n      other: Edit tag description without review\n    rank_tag_synonym_label:\n      other: Manage tag synonyms\n  email:\n    other: 電子郵件\n  e_mail:\n    other: 電子郵件\n  password:\n    other: 密碼\n  pass:\n    other: 密碼\n  old_pass:\n    other: 目前密碼\n  original_text:\n    other: 此貼文\n  email_or_password_wrong_error:\n    other: 電郵和密碼不匹配。\n  error:\n    common:\n      invalid_url:\n        other: URL 無效。\n      status_invalid:\n        other: 無效狀態。\n    password:\n      space_invalid:\n        other: 密碼不能包含空白字元。\n    admin:\n      cannot_update_their_password:\n        other: 你不能修改自己的密码。\n      cannot_edit_their_profile:\n        other: You cannot modify your profile.\n      cannot_modify_self_status:\n        other: You cannot modify your status.\n      email_or_password_wrong:\n        other: 電郵和密碼不匹配。\n    answer:\n      not_found:\n        other: 未發現答案。\n      cannot_deleted:\n        other: 沒有刪除權限。\n      cannot_update:\n        other: 沒有更新權限。\n      question_closed_cannot_add:\n        other: Questions are closed and cannot be added.\n      content_cannot_empty:\n        other: Answer content cannot be empty.\n    comment:\n      edit_without_permission:\n        other: 不允許編輯留言。\n      not_found:\n        other: 未發現留言。\n      cannot_edit_after_deadline:\n        other: 這則留言時間過久，無法修改。\n      content_cannot_empty:\n        other: Comment content cannot be empty.\n    email:\n      duplicate:\n        other: 這個電子郵件地址已被使用。\n      need_to_be_verified:\n        other: 需驗證電子郵件地址。\n      verify_url_expired:\n        other: 電子郵件地址驗證網址已過期，請重寄電子郵件。\n      illegal_email_domain_error:\n        other: Email is not allowed from that email domain. Please use another one.\n    lang:\n      not_found:\n        other: 未找到語言檔。\n    object:\n      captcha_verification_failed:\n        other: 驗證碼錯誤。\n      disallow_follow:\n        other: 你不被允許追蹤。\n      disallow_vote:\n        other: 你無法投票。\n      disallow_vote_your_self:\n        other: 你不能為自己的貼文投票。\n      not_found:\n        other: 找不到物件。\n      verification_failed:\n        other: 驗證失敗。\n      email_or_password_incorrect:\n        other: 電子郵件地址和密碼不匹配。\n      old_password_verification_failed:\n        other: 舊密碼驗證失敗\n      new_password_same_as_previous_setting:\n        other: 新密碼與先前的一樣。\n      already_deleted:\n        other: 這則貼文已被刪除。\n    meta:\n      object_not_found:\n        other: Meta object not found\n    question:\n      already_deleted:\n        other: This post has been deleted.\n      under_review:\n        other: Your post is awaiting review. It will be visible after it has been approved.\n      not_found:\n        other: 找不到問題。\n      cannot_deleted:\n        other: 無刪除權限。\n      cannot_close:\n        other: 無關閉權限。\n      cannot_update:\n        other: 無更新權限。\n      content_cannot_empty:\n        other: Content cannot be empty.\n      content_less_than_minimum:\n        other: Not enough content entered.\n    rank:\n      fail_to_meet_the_condition:\n        other: Reputation rank fail to meet the condition.\n      vote_fail_to_meet_the_condition:\n        other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.\n      no_enough_rank_to_operate:\n        other: You need at least {{.Rank}} reputation to do this.\n    report:\n      handle_failed:\n        other: 報告處理失敗。\n      not_found:\n        other: 找不到報告。\n    tag:\n      already_exist:\n        other: Tag already exists.\n      not_found:\n        other: 找不到標籤。\n      recommend_tag_not_found:\n        other: Recommend tag is not exist.\n      recommend_tag_enter:\n        other: 請輸入至少一個必需的標籤。\n      not_contain_synonym_tags:\n        other: 不應包含同義詞標籤。\n      cannot_update:\n        other: 沒有權限更新。\n      is_used_cannot_delete:\n        other: You cannot delete a tag that is in use.\n      cannot_set_synonym_as_itself:\n        other: 你不能將目前標籤的同義詞設定為本身。\n      minimum_count:\n        other: Not enough tags were entered.\n    smtp:\n      config_from_name_cannot_be_email:\n        other: The from name cannot be a email address.\n    theme:\n      not_found:\n        other: 未找到主題。\n    revision:\n      review_underway:\n        other: 目前無法編輯，有一個版本在審查佇列中。\n      no_permission:\n        other: No permission to revise.\n    user:\n      external_login_missing_user_id:\n        other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.\n      external_login_unbinding_forbidden:\n        other: Please set a login password for your account before you remove this login.\n      email_or_password_wrong:\n        other:\n          other: 電子郵箱和密碼不匹配。\n      not_found:\n        other: 未找到使用者。\n      suspended:\n        other: 該使用者已被停權。\n      username_invalid:\n        other: 使用者名稱無效。\n      username_duplicate:\n        other: 使用者名稱已被使用。\n      set_avatar:\n        other: 大頭照設定錯誤。\n      cannot_update_your_role:\n        other: 您不能修改自己的角色。\n      not_allowed_registration:\n        other: Currently the site is not open for registration.\n      not_allowed_login_via_password:\n        other: Currently the site is not allowed to login via password.\n      access_denied:\n        other: Access denied\n      page_access_denied:\n        other: You do not have access to this page.\n      add_bulk_users_format_error:\n        other: \"Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}\"\n      add_bulk_users_amount_error:\n        other: \"The number of users you add at once should be in the range of 1-{{.MaxAmount}}.\"\n      status_suspended_forever:\n        other: \"<strong>This user was suspended forever.</strong> This user doesn't meet a community guideline.\"\n      status_suspended_until:\n        other: \"<strong>This user was suspended until {{.SuspendedUntil}}.</strong> This user doesn't meet a community guideline.\"\n      status_deleted:\n        other: \"This user was deleted.\"\n      status_inactive:\n        other: \"This user is inactive.\"\n    config:\n      read_config_failed:\n        other: 讀取組態失敗\n    database:\n      connection_failed:\n        other: 資料庫連線失敗\n      create_table_failed:\n        other: 表建立失敗\n    install:\n      create_config_failed:\n        other: 無法建立 config.yaml 檔。\n    upload:\n      unsupported_file_format:\n        other: 不支援的檔案格式。\n    site_info:\n      config_not_found:\n        other: Site config not found.\n    badge:\n      object_not_found:\n        other: Badge object not found\n  reason:\n    spam:\n      name:\n        other: 垃圾訊息\n      desc:\n        other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\n    rude_or_abusive:\n      name:\n        other: rude or abusive\n      desc:\n        other: \"A reasonable person would find this content inappropriate for respectful discourse.\"\n    a_duplicate:\n      name:\n        other: a duplicate\n      desc:\n        other: This question has been asked before and already has an answer.\n      placeholder:\n        other: Enter the existing question link\n    not_a_answer:\n      name:\n        other: not an answer\n      desc:\n        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.\"\n    no_longer_needed:\n      name:\n        other: no longer needed\n      desc:\n        other: This comment is outdated, conversational or not relevant to this post.\n    something:\n      name:\n        other: something else\n      desc:\n        other: This post requires staff attention for another reason not listed above.\n      placeholder:\n        other: Let us know specifically what you are concerned about\n    community_specific:\n      name:\n        other: a community-specific reason\n      desc:\n        other: This question doesn't meet a community guideline.\n    not_clarity:\n      name:\n        other: needs details or clarity\n      desc:\n        other: This question currently includes multiple questions in one. It should focus on one problem only.\n    looks_ok:\n      name:\n        other: looks OK\n      desc:\n        other: This post is good as-is and not low quality.\n    needs_edit:\n      name:\n        other: needs edit, and I did it\n      desc:\n        other: Improve and correct problems with this post yourself.\n    needs_close:\n      name:\n        other: 需關閉\n      desc:\n        other: A closed question can't answer, but still can edit, vote and comment.\n    needs_delete:\n      name:\n        other: needs delete\n      desc:\n        other: This post will be deleted.\n  question:\n    close:\n      duplicate:\n        name:\n          other: 垃圾訊息\n        desc:\n          other: 此問題以前就有人問過，而且已經有了答案。\n      guideline:\n        name:\n          other: 一个社群特定原因\n        desc:\n          other: 此問題不符合社群準則。\n      multiple:\n        name:\n          other: 需要細節或明晰\n        desc:\n          other: This question currently includes multiple questions in one. It should focus on one problem only.\n      other:\n        name:\n          other: 其他原因\n        desc:\n          other: 這個帖子需要上面沒有列出的另一個原因。\n    operation_type:\n      asked:\n        other: 提問於\n      answered:\n        other: 回答於\n      modified:\n        other: 修改於\n    deleted_title:\n      other: Deleted question\n    questions_title:\n      other: Questions\n  tag:\n    tags_title:\n      other: Tags\n    no_description:\n      other: The tag has no description.\n  notification:\n    action:\n      update_question:\n        other: 更新了問題\n      answer_the_question:\n        other: 回答了問題\n      update_answer:\n        other: 更新了答案\n      accept_answer:\n        other: 已接受的回答\n      comment_question:\n        other: 留言了問題\n      comment_answer:\n        other: 留言了答案\n      reply_to_you:\n        other: 回覆了你\n      mention_you:\n        other: 提到了你\n      your_question_is_closed:\n        other: 你的問題已被關閉\n      your_question_was_deleted:\n        other: 你的問題已被刪除\n      your_answer_was_deleted:\n        other: 你的答案已被刪除\n      your_comment_was_deleted:\n        other: 你的留言已被刪除\n      up_voted_question:\n        other: upvoted question\n      down_voted_question:\n        other: downvoted question\n      up_voted_answer:\n        other: upvoted answer\n      down_voted_answer:\n        other: downvoted answer\n      up_voted_comment:\n        other: upvoted comment\n      invited_you_to_answer:\n        other: invited you to answer\n      earned_badge:\n        other: You've earned the \"{{.BadgeName}}\" badge\n  email_tpl:\n    change_email:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new email address\"\n      body:\n        other: \"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    new_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n      body:\n        other: \"<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    invited_you_to_answer:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} invited you to answer\"\n      body:\n        other: \"<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>I think you may know the answer.</blockquote><br>\\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_comment:\n      title:\n        other: \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n      body:\n        other: \"<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\\n\\n{{.DisplayName}}:<br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    new_question:\n      title:\n        other: \"[{{.SiteName}}] New question: {{.QuestionTitle}}\"\n      body:\n        other: \"<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\\n<small>{{.Tags}}</small><br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\\n\\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n    pass_reset:\n      title:\n        other: \"[{{.SiteName }}] Password reset\"\n      body:\n        other: \"Somebody asked to reset your password on {{.SiteName}}.<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    register:\n      title:\n        other: \"[{{.SiteName}}] Confirm your new account\"\n      body:\n        other: \"Welcome to {{.SiteName}}!<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n    test:\n      title:\n        other: \"[{{.SiteName}}] Test Email\"\n      body:\n        other: \"This is a test email.\\n<br><br>\\n\\n--<br>\\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.\"\n  action_activity_type:\n    upvote:\n      other: upvote\n    upvoted:\n      other: upvoted\n    downvote:\n      other: downvote\n    downvoted:\n      other: downvoted\n    accept:\n      other: 採納\n    accepted:\n      other: 已採納\n    edit:\n      other: 編輯\n  review:\n    queued_post:\n      other: Queued post\n    flagged_post:\n      other: Flagged post\n    suggested_post_edit:\n      other: Suggested edits\n  reaction:\n    tooltip:\n      other: \"{{ .Names }} and {{ .Count }} more...\"\n  badge:\n    default_badges:\n      autobiographer:\n        name:\n          other: Autobiographer\n        desc:\n          other: Filled out <a href=\"{{ .ProfileURL }}\" target=\"_blank\">profile</a> information.\n      certified:\n        name:\n          other: Certified\n        desc:\n          other: Completed our new user tutorial.\n      editor:\n        name:\n          other: 編輯者\n        desc:\n          other: First post edit.\n      first_flag:\n        name:\n          other: First Flag\n        desc:\n          other: First flagged a post.\n      first_upvote:\n        name:\n          other: First Upvote\n        desc:\n          other: First up voted a post.\n      first_link:\n        name:\n          other: 首個連結\n        desc:\n          other: First added a link to another post.\n      first_reaction:\n        name:\n          other: First Reaction\n        desc:\n          other: First reacted to the post.\n      first_share:\n        name:\n          other: First Share\n        desc:\n          other: First shared a post.\n      scholar:\n        name:\n          other: Scholar\n        desc:\n          other: Asked a question and accepted an answer.\n      commentator:\n        name:\n          other: Commentator\n        desc:\n          other: Leave 5 comments.\n      new_user_of_the_month:\n        name:\n          other: New User of the Month\n        desc:\n          other: Outstanding contributions in their first month.\n      read_guidelines:\n        name:\n          other: Read Guidelines\n        desc:\n          other: Read the [community guidelines].\n      reader:\n        name:\n          other: 閱讀者\n        desc:\n          other: Read every answers in a topic with more than 10 answers.\n      welcome:\n        name:\n          other: 歡迎\n        desc:\n          other: Received a up vote.\n      nice_share:\n        name:\n          other: Nice Share\n        desc:\n          other: Shared a post with 25 unique visitors.\n      good_share:\n        name:\n          other: Good Share\n        desc:\n          other: Shared a post with 300 unique visitors.\n      great_share:\n        name:\n          other: Great Share\n        desc:\n          other: Shared a post with 1000 unique visitors.\n      out_of_love:\n        name:\n          other: Out of Love\n        desc:\n          other: Used 50 up votes in a day.\n      higher_love:\n        name:\n          other: Higher Love\n        desc:\n          other: Used 50 up votes in a day 5 times.\n      crazy_in_love:\n        name:\n          other: Crazy in Love\n        desc:\n          other: Used 50 up votes in a day 20 times.\n      promoter:\n        name:\n          other: Promoter\n        desc:\n          other: Invited a user.\n      campaigner:\n        name:\n          other: Campaigner\n        desc:\n          other: Invited 3 basic users.\n      champion:\n        name:\n          other: Champion\n        desc:\n          other: Invited 5 members.\n      thank_you:\n        name:\n          other: 感謝\n        desc:\n          other: Has 20 up voted posts and gave 10 up votes.\n      gives_back:\n        name:\n          other: Gives Back\n        desc:\n          other: Has 100 up voted posts and gave 100 up votes.\n      empathetic:\n        name:\n          other: Empathetic\n        desc:\n          other: Has 500 up voted posts and gave 1000 up votes.\n      enthusiast:\n        name:\n          other: Enthusiast\n        desc:\n          other: Visited 10 consecutive days.\n      aficionado:\n        name:\n          other: Aficionado\n        desc:\n          other: Visited 100 consecutive days.\n      devotee:\n        name:\n          other: Devotee\n        desc:\n          other: Visited 365 consecutive days.\n      anniversary:\n        name:\n          other: Anniversary\n        desc:\n          other: Active member for a year, posted at least once.\n      appreciated:\n        name:\n          other: Appreciated\n        desc:\n          other: Received 1 up vote on 20 posts.\n      respected:\n        name:\n          other: Respected\n        desc:\n          other: Received 2 up votes on 100 posts.\n      admired:\n        name:\n          other: Admired\n        desc:\n          other: Received 5 up votes on 300 posts.\n      solved:\n        name:\n          other: Solved\n        desc:\n          other: Have an answer be accepted.\n      guidance_counsellor:\n        name:\n          other: Guidance Counsellor\n        desc:\n          other: Have 10 answers be accepted.\n      know_it_all:\n        name:\n          other: Know-it-All\n        desc:\n          other: Have 50 answers be accepted.\n      solution_institution:\n        name:\n          other: Solution Institution\n        desc:\n          other: Have 150 answers be accepted.\n      nice_answer:\n        name:\n          other: Nice Answer\n        desc:\n          other: Answer score of 10 or more.\n      good_answer:\n        name:\n          other: Good Answer\n        desc:\n          other: Answer score of 25 or more.\n      great_answer:\n        name:\n          other: Great Answer\n        desc:\n          other: Answer score of 50 or more.\n      nice_question:\n        name:\n          other: Nice Question\n        desc:\n          other: Question score of 10 or more.\n      good_question:\n        name:\n          other: Good Question\n        desc:\n          other: Question score of 25 or more.\n      great_question:\n        name:\n          other: Great Question\n        desc:\n          other: Question score of 50 or more.\n      popular_question:\n        name:\n          other: Popular Question\n        desc:\n          other: Question with 500 views.\n      notable_question:\n        name:\n          other: Notable Question\n        desc:\n          other: Question with 1,000 views.\n      famous_question:\n        name:\n          other: Famous Question\n        desc:\n          other: Question with 5,000 views.\n      popular_link:\n        name:\n          other: Popular Link\n        desc:\n          other: Posted an external link with 50 clicks.\n      hot_link:\n        name:\n          other: Hot Link\n        desc:\n          other: Posted an external link with 300 clicks.\n      famous_link:\n        name:\n          other: Famous Link\n        desc:\n          other: Posted an external link with 100 clicks.\n    default_badge_groups:\n      getting_started:\n        name:\n          other: Getting Started\n      community:\n        name:\n          other: Community\n      posting:\n        name:\n          other: Posting\n# The following fields are used for interface presentation(Front-end)\nui:\n  how_to_format:\n    title: 如何設定文字格式\n    desc: >-\n      <ul class=\"mb-0\"><li><p class=\"mb-2\">mention a post: <code>#post_id</code></p></li> <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>\n  pagination:\n    prev: 上一頁\n    next: 下一頁\n  page_title:\n    question: 問題\n    questions: 問題\n    tag: 標籤\n    tags: 標籤\n    tag_wiki: 標籤 wiki\n    create_tag: Create Tag\n    edit_tag: 編輯標籤\n    ask_a_question: Create Question\n    edit_question: 編輯問題\n    edit_answer: 編輯回答\n    search: 搜尋\n    posts_containing: 包含的貼文\n    settings: 設定\n    notifications: 通知\n    login: 登入\n    sign_up: 註冊\n    account_recovery: 帳號恢復\n    account_activation: 帳號啟用\n    confirm_email: 確認電子郵件\n    account_suspended: 帳號已被停權\n    admin: 後台管理\n    change_email: 修改電子郵件\n    install: Answer 安裝\n    upgrade: Answer 升級\n    maintenance: 網站維護\n    users: 使用者\n    oauth_callback: Processing\n    http_404: HTTP 錯誤 404\n    http_50X: HTTP 錯誤 500\n    http_403: HTTP 錯誤 403\n    logout: 登出\n    posts: Posts\n    ai_assistant: AI Assistant\n  ai_assistant:\n    description: Got a question? Ask it and get answers, perspectives, and recommendations.\n    recent_conversations: Recent Conversations\n    show_more: Show more\n    new: New chat\n    ai_generate: AI-generated from posts and may not be accurate.\n    copy: Copy\n    ask_a_follow_up: Ask a follow-up\n    ask_placeholder: Ask a question\n  notifications:\n    title: 通知\n    inbox: 收件夾\n    achievement: 成就\n    new_alerts: New alerts\n    all_read: 全部標記為已讀\n    show_more: 顯示更多\n    someone: Someone\n    inbox_type:\n      all: 所有\n      posts: Posts\n      invites: Invites\n      votes: Votes\n    answer: Answer\n    question: Question\n    badge_award: Badge\n  suspended:\n    title: 您的帳號已被停權\n    until_time: \"你的帳號被停權至{{ time }}。\"\n    forever: 你的帳號已被永久停權。\n    end: 違反了我們的社群準則。\n    contact_us: Contact us\n  editor:\n    blockquote:\n      text: 引用\n    bold:\n      text: 粗體\n    chart:\n      text: 圖表\n      flow_chart: 流程圖\n      sequence_diagram: 時序圖\n      class_diagram: 類圖\n      state_diagram: 狀態圖\n      entity_relationship_diagram: 實體關係圖\n      user_defined_diagram: 用戶自定義圖表\n      gantt_chart: 甘特圖\n      pie_chart: 圓餅圖\n    code:\n      text: 代碼示例\n      add_code: 添加代碼示例\n      form:\n        fields:\n          code:\n            label: 代碼塊\n            msg:\n              empty: 代碼不能為空\n          language:\n            label: 語言\n            placeholder: 自動偵測\n      btn_cancel: 取消\n      btn_confirm: 添加\n    formula:\n      text: 公式\n      options:\n        inline: 內聯公式\n        block: 公式塊\n    heading:\n      text: 標題\n      options:\n        h1: 標題 1\n        h2: 標題 2\n        h3: 標題 3\n        h4: 標題 4\n        h5: 標題 5\n        h6: 標題 6\n    help:\n      text: 幫助\n    hr:\n      text: Horizontal rule\n    image:\n      text: 圖片\n      add_image: 添加圖片\n      tab_image: 上傳圖片\n      form_image:\n        fields:\n          file:\n            label: 圖檔\n            btn: 選擇圖片\n            msg:\n              empty: 文件不能為空。\n              only_image: 只能上傳圖片文件。\n              max_size: File size cannot exceed {{size}} MB.\n          desc:\n            label: 圖片描述\n      tab_url: 圖片地址\n      form_url:\n        fields:\n          url:\n            label: 圖片地址\n            msg:\n              empty: 圖片地址不能為空\n          name:\n            label: 圖片描述\n      btn_cancel: 取消\n      btn_confirm: 添加\n      uploading: 上傳中...\n    indent:\n      text: 增加縮排\n    outdent:\n      text: 減少縮排\n    italic:\n      text: 斜體\n    link:\n      text: 超連結\n      add_link: 添加超連結\n      form:\n        fields:\n          url:\n            label: 連結\n            msg:\n              empty: 連結不能為空。\n          name:\n            label: 描述\n      btn_cancel: 取消\n      btn_confirm: 添加\n    ordered_list:\n      text: Numbered list\n    unordered_list:\n      text: Bulleted list\n    table:\n      text: 表格\n      heading: 表頭\n      cell: 單元格\n    file:\n      text: Attach files\n      not_supported: \"Don’t support that file type. Try again with {{file_type}}.\"\n      max_size: \"Attach files size cannot exceed {{size}} MB.\"\n  close_modal:\n    title: 關閉原因是...\n    btn_cancel: 取消\n    btn_submit: 提交\n    remark:\n      empty: 不能為空。\n    msg:\n      empty: 請選擇一個原因。\n  report_modal:\n    flag_title: 報告為...\n    close_title: 關閉原因是...\n    review_question_title: 審核問題\n    review_answer_title: 審核回答\n    review_comment_title: 審核評論\n    btn_cancel: 取消\n    btn_submit: 提交\n    remark:\n      empty: 不能為空\n    msg:\n      empty: 請選擇一個原因。\n      not_a_url: URL format is incorrect.\n      url_not_match: URL origin does not match the current website.\n  tag_modal:\n    title: 創建新標籤\n    form:\n      fields:\n        display_name:\n          label: Display name\n          msg:\n            empty: 顯示名稱不能為空。\n            range: 顯示名稱不能超過 35 個字符。\n        slug_name:\n          label: URL slug\n          desc: URL slug up to 35 characters.\n          msg:\n            empty: URL 固定連結不能為空。\n            range: URL 固定連結不能超過 35 個字元。\n            character: URL 固定連結包含非法字元。\n        desc:\n          label: 描述\n        revision:\n          label: Revision\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)\n    btn_cancel: 取消\n    btn_submit: 提交\n    btn_post: Post new tag\n  tag_info:\n    created_at: 創建於\n    edited_at: 編輯於\n    history: 歷史\n    synonyms:\n      title: 同義詞\n      text: 以下標籤等同於\n      empty: 此標籤目前沒有同義詞。\n      btn_add: 添加同義詞\n      btn_edit: 編輯\n      btn_save: 儲存\n    synonyms_text: 以下標籤等同於\n    delete:\n      title: 刪除標籤\n      tip_with_posts: >-\n        <p>We do not allow <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>\n      tip_with_synonyms: >-\n        <p>We do not allow <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>\n      tip: 你確定要刪除嗎？\n      close: 關閉\n    merge:\n      title: Merge tag\n      source_tag_title: Source tag\n      source_tag_description: The source tag and its associated data will be remapped to the target tag.\n      target_tag_title: Target tag\n      target_tag_description: A synonym between these two tags will be created after merging.\n      no_results: No tags matched\n      btn_submit: 送出\n      btn_close: 關閉\n  edit_tag:\n    title: 編輯標籤\n    default_reason: 編輯標籤\n    default_first_reason: Add tag\n    btn_save_edits: 儲存更改\n    btn_cancel: 取消\n  dates:\n    long_date: MM月DD日\n    long_date_with_year: \"YYYY年MM月DD日\"\n    long_date_with_time: \"YYYY 年 MM 月 DD 日 HH:mm\"\n    now: 剛剛\n    x_seconds_ago: \"{{count}} 秒前\"\n    x_minutes_ago: \"{{count}} 分鐘前\"\n    x_hours_ago: \"{{count}} 小時前\"\n    hour: 小時\n    day: 天\n    hours: hours\n    days: days\n    month: month\n    months: months\n    year: year\n  reaction:\n    heart: heart\n    smile: smile\n    frown: frown\n    btn_label: add or remove reactions\n    undo_emoji: undo {{ emoji }} reaction\n    react_emoji: react with {{ emoji }}\n    unreact_emoji: unreact with {{ emoji }}\n  comment:\n    btn_add_comment: 添加評論\n    reply_to: 回復\n    btn_reply: 回復\n    btn_edit: 編輯\n    btn_delete: 刪除\n    btn_flag: 舉報\n    btn_save_edits: 保存\n    btn_cancel: 取消\n    show_more: \"{{count}} 條剩餘評論\"\n    tip_question: >-\n      通过評論询问更多问题或提出改進建議。避免在評論中回答問題。\n    tip_answer: >-\n      使用評論回復其他用戶或通知他們进行更改。如果你要添加新的信息，請編輯你的帖子，而不是發表評論。\n    tip_vote: It adds something useful to the post\n  edit_answer:\n    title: 編輯回答\n    default_reason: 編輯回答\n    default_first_reason: Add answer\n    form:\n      fields:\n        revision:\n          label: 編輯歷史\n        answer:\n          label: 回答內容\n          feedback:\n            characters: 內容必須至少6個字元長度。\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            簡單描述更改原因 (錯別字、文字表達、格式等等)\n    btn_save_edits: 儲存更改\n    btn_cancel: 取消\n  tags:\n    title: 標籤\n    sort_buttons:\n      popular: 熱門\n      name: 名稱\n      newest: Newest\n    button_follow: 關注\n    button_following: 已關注\n    tag_label: 個問題\n    search_placeholder: 通過標籤名過濾\n    no_desc: 此標籤無描述。\n    more: 更多\n    wiki: Wiki\n  ask:\n    title: Create Question\n    edit_title: 編輯問題\n    default_reason: 編輯問題\n    default_first_reason: Create question\n    similar_questions: 相似的問題\n    form:\n      fields:\n        revision:\n          label: 編輯歷史\n        title:\n          label: 標題\n          placeholder: What's your topic? Be specific.\n          msg:\n            empty: 標題不能為空\n            range: 標題最多 150 個字元\n        body:\n          label: 正文\n          msg:\n            empty: 正文不能爲空。\n          hint:\n            optional_body: Describe what the question is about.\n            minimum_characters: \"Describe what the question is about, at least {{min_content_length}} characters are required.\"\n        tags:\n          label: 標籤\n          msg:\n            empty: 標籤不能為空\n        answer:\n          label: 回答內容\n          msg:\n            empty: 回答內容不能為空\n        edit_summary:\n          label: Edit summary\n          placeholder: >-\n            簡單描述更改原因 (錯別字、文字表達、格式等等)\n    btn_post_question: 提出問題\n    btn_save_edits: 儲存更改\n    answer_question: 回答您自己的問題\n    post_question&answer: 發布您的問題和答案\n  tag_selector:\n    add_btn: 建立標籤\n    create_btn: 建立新標籤\n    search_tag: 搜尋標籤\n    hint: Describe what your content is about, at least one tag is required.\n    hint_zero_tags: Describe what your content is about.\n    hint_more_than_one_tag: \"Describe what your content is about, at least {{min_tags_number}} tags are required.\"\n    no_result: 沒有匹配的標籤\n    tag_required_text: 必填標籤 (至少一個)\n  header:\n    nav:\n      question: 問題\n      tag: 標籤\n      user: 用戶\n      badges: Badges\n      profile: 用戶主頁\n      setting: 帳號設置\n      logout: 登出\n      admin: 後台管理\n      review: 審查\n      bookmark: Bookmarks\n      moderation: Moderation\n    search:\n      placeholder: 搜尋\n  footer:\n    build_on: Powered by <1> Apache Answer </1>\n  upload_img:\n    name: 更改\n    loading: 讀取中...\n  pic_auth_code:\n    title: 驗證碼\n    placeholder: 輸入上面的文字\n    msg:\n      empty: 验证码不能為空\n  inactive:\n    first: >-\n      就差一步！我們寄送了一封啟用電子郵件到 <bold>{{mail}}</bold>。請按照郵件中的說明啟用您的帳戶。\n    info: \"如果沒有收到，請檢查您的垃圾郵件文件夾。\"\n    another: >-\n      我們向您發送了另一封啟用電子郵件，地址為 <bold>{{mail}}</bold>。它可能需要幾分鐘才能到達；請務必檢查您的垃圾郵件文件夾。\n    btn_name: 重新發送啟用郵件\n    change_btn_name: 更改郵箱\n    msg:\n      empty: 不能為空\n    resend_email:\n      url_label: Are you sure you want to resend the activation email?\n      url_text: You can also give the activation link above to the user.\n  login:\n    login_to_continue: 登入以繼續\n    info_sign: 沒有帳戶？<1>註冊</1>\n    info_login: 已經有一個帳號？<1>登入</1>\n    agreements: 登入即表示您同意<1>隱私政策</1>和<3>服務條款</3>。\n    forgot_pass: 忘記密碼?\n    name:\n      label: 名稱\n      msg:\n        empty: 名稱不能為空\n        range: Name must be between 2 to 30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n    email:\n      label: 郵箱\n      msg:\n        empty: 郵箱不能為空\n    password:\n      label: 密碼\n      msg:\n        empty: 密碼不能為空\n        different: 兩次輸入密碼不一致\n  account_forgot:\n    page_title: 忘記密碼\n    btn_name: 向我發送恢復郵件\n    send_success: >-\n      如果帳號與<strong>{{mail}}</strong>相符，您應該很快就會收到一封電子郵件，說明如何重置您的密碼。\n    email:\n      label: 郵箱\n      msg:\n        empty: 郵箱不能為空\n  change_email:\n    btn_cancel: 取消\n    btn_update: 更新電子郵件地址\n    send_success: >-\n      如果帳號與<strong>{{mail}}</strong>相符，您應該很快就會收到一封電子郵件，說明如何重置您的密碼。\n    email:\n      label: New email\n      msg:\n        empty: 郵箱不能為空\n  oauth:\n    connect: Connect with {{ auth_name }}\n    remove: Remove {{ auth_name }}\n  oauth_bind_email:\n    subtitle: Add a recovery email to your account.\n    btn_update: Update email address\n    email:\n      label: Email\n      msg:\n        empty: Email cannot be empty.\n    modal_title: Email already existes.\n    modal_content: This email address already registered. Are you sure you want to connect to the existing account?\n    modal_cancel: Change email\n    modal_confirm: Connect to the existing account\n  password_reset:\n    page_title: 密碼重置\n    btn_name: 重置我的密碼\n    reset_success: >-\n      你已經成功更改密碼，將返回登入頁面\n    link_invalid: >-\n      抱歉，此密碼重置連結已失效。也許是你已經重置過密碼了？\n    to_login: 前往登入頁面\n    password:\n      label: 密碼\n      msg:\n        empty: 密碼不能為空\n        length: 密碼長度在8-32個字元之間\n        different: 兩次輸入密碼不一致\n    password_confirm:\n      label: Confirm new password\n  settings:\n    page_title: 設置\n    goto_modify: Go to modify\n    nav:\n      profile: 我的資料\n      notification: 通知\n      account: 帳號\n      interface: 界面\n    profile:\n      heading: 個人資料\n      btn_name: 保存\n      display_name:\n        label: Display name\n        msg: 顯示名稱不能為空。\n        msg_range: Display name must be 2-30 characters in length.\n      username:\n        label: 用戶名\n        caption: 用戶之間可以通過 \"@用戶名\" 進行交互。\n        msg: 用戶名不能為空\n        msg_range: Username must be 2-30 characters in length.\n        character: 'Must use the character set \"a-z\", \"0-9\", \"- . _\"'\n      avatar:\n        label: Profile image\n        gravatar: 頭像\n        gravatar_text: You can change image on\n        custom: 自定義\n        custom_text: 您可以上傳您的圖片。\n        default: 系統\n        msg: 請上傳頭像\n      bio:\n        label: About me\n      website:\n        label: 網站\n        placeholder: \"https://example.com\"\n        msg: 網站格式不正確\n      location:\n        label: 位置\n        placeholder: \"城市, 國家\"\n    notification:\n      heading: 通知\n      turn_on: Turn on\n      inbox:\n        label: Email notifications\n        description: Answers to your questions, comments, invites, and more.\n      all_new_question:\n        label: All new questions\n        description: Get notified of all new questions. Up to 50 questions per week.\n      all_new_question_for_following_tags:\n        label: All new questions for following tags\n        description: Get notified of new questions for following tags.\n    account:\n      heading: 帳號\n      change_email_btn: 更改郵箱\n      change_pass_btn: 更改密碼\n      change_email_info: >-\n        我們已經寄出一封郵件至此電子郵件地址，請遵照說明進行確認。\n      email:\n        label: 電子郵件地址\n      new_email:\n        label: 新電子郵件地址\n        msg: 新電子郵件地址不能為空白。\n      pass:\n        label: 目前密碼\n        msg: Password cannot be empty.\n      password_title: 密碼\n      current_pass:\n        label: Current password\n        msg:\n          empty: Current password cannot be empty.\n          length: 密碼長度必須在 8 至 32 之間\n          different: 兩次輸入的密碼不匹配\n      new_pass:\n        label: New password\n      pass_confirm:\n        label: 確認新密碼\n    interface:\n      heading: 介面\n      lang:\n        label: 介面語言\n        text: 設定使用者介面語言，在重新整裡頁面後生效。\n    my_logins:\n      title: 我的登入\n      label: 使用這些帳號登入或註冊此網站。\n      modal_title: 移除登入\n      modal_content: Are you sure you want to remove this login from your account?\n      modal_confirm_btn: Remove\n      remove_success: Removed successfully\n  toast:\n    update: 更新成功\n    update_password: 更改密碼成功。\n    flag_success: 感謝您的標記\n    forbidden_operate_self: 禁止自己操作\n    review: 您的修訂將在審核通過後顯示。\n    sent_success: Sent successfully\n  related_question:\n    title: Related\n    answers: 個回答\n  linked_question:\n    title: Linked\n    description: Posts linked to\n    no_linked_question: No contents linked from this content.\n  invite_to_answer:\n    title: People Asked\n    desc: Invite people who you think might know the answer.\n    invite: Invite to answer\n    add: Add people\n    search: Search people\n  question_detail:\n    action: Action\n    created: Created\n    Asked: 提問於\n    asked: 提問於\n    update: 修改於\n    Edited: Edited\n    edit: 最後編輯於\n    commented: commented\n    Views: 閱讀次數\n    Follow: 關注\n    Following: 已關注\n    follow_tip: Follow this question to receive notifications\n    answered: 回答於\n    closed_in: 關閉於\n    show_exist: 顯示現有問題。\n    useful: Useful\n    question_useful: It is useful and clear\n    question_un_useful: It is unclear or not useful\n    question_bookmark: Bookmark this question\n    answer_useful: It is useful\n    answer_un_useful: It is not useful\n    answers:\n      title: 個回答\n      score: 評分\n      newest: 最新\n      oldest: Oldest\n      btn_accept: 採納\n      btn_accepted: 已被採納\n    write_answer:\n      title: 你的回答\n      edit_answer: Edit my existing answer\n      btn_name: 提交你的回答\n      add_another_answer: 添加另一個答案\n      confirm_title: 繼續回答\n      continue: 繼續\n      confirm_info: >-\n        <p>您確定要添加一個新的回答嗎？</p><p>您可以使用编辑链接来完善和改进您现有的答案。</p>\n      empty: 回答內容不能為空。\n      characters: 內容必須至少6個字元長度。\n      tips:\n        header_1: Thanks for your answer\n        li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.\n        li1_2: Back up any statements you make with references or personal experience.\n        header_2: But <strong>avoid</strong> ...\n        li2_1: Asking for help, seeking clarification, or responding to other answers.\n    reopen:\n      confirm_btn: Reopen\n      title: 重新打開這個貼文\n      content: 確定要重新打開嗎？\n    list:\n      confirm_btn: List\n      title: List this post\n      content: Are you sure you want to list?\n    unlist:\n      confirm_btn: Unlist\n      title: Unlist this post\n      content: Are you sure you want to unlist?\n    pin:\n      title: Pin this post\n      content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.\n      confirm_btn: Pin\n  delete:\n    title: 刪除此貼\n    question: >-\n      我們不建議<strong>刪除有回答的貼文</strong>。因為這樣做會使得後來的讀者無法從該問題中獲得幫助。</p><p>如果刪除過多有回答的貼文，你的帳號將會被禁止提問。你確定要刪除嗎？\n    answer_accepted: >-\n      <p>我們不建議<strong>刪除被採納的回答</strong>。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。</p>如果刪除過多被採納的貼文，你的帳號將會被禁止回答任何提問。你確定要刪除嗎？\n    other: 你確定要刪除？\n    tip_answer_deleted: 此回答已被刪除\n    undelete_title: Undelete this post\n    undelete_desc: Are you sure you wish to undelete?\n  btns:\n    confirm: 確認\n    cancel: 取消\n    edit: 編輯\n    save: 儲存\n    delete: 刪除\n    undelete: 還原\n    list: 清單\n    unlist: Unlist\n    unlisted: Unlisted\n    login: 登入\n    signup: 註冊\n    logout: 登出\n    verify: 驗證\n    create: 建立\n    approve: 核准\n    reject: 拒絕\n    skip: 略過\n    discard_draft: Discard draft\n    pinned: Pinned\n    all: All\n    question: Question\n    answer: Answer\n    comment: Comment\n    refresh: Refresh\n    resend: Resend\n    deactivate: Deactivate\n    active: Active\n    suspend: Suspend\n    unsuspend: Unsuspend\n    close: Close\n    reopen: Reopen\n    ok: OK\n    light: Light\n    dark: Dark\n    system_setting: System setting\n    default: Default\n    reset: Reset\n    tag: Tag\n    post_lowercase: post\n    filter: Filter\n    ignore: Ignore\n    submit: Submit\n    normal: Normal\n    closed: Closed\n    deleted: Deleted\n    deleted_permanently: Deleted permanently\n    pending: Pending\n    more: More\n    view: View\n    card: Card\n    compact: Compact\n    display_below: Display below\n    always_display: Always display\n    or: or\n    back_sites: Back to sites\n  search:\n    title: 搜尋結果\n    keywords: 關鍵詞\n    options: 選項\n    follow: 追蹤\n    following: 已關注\n    counts: \"{{count}} 個結果\"\n    counts_loading: \"... Results\"\n    more: 更多\n    sort_btns:\n      relevance: 相關性\n      newest: 最新的\n      active: 活躍的\n      score: 評分\n      more: 更多\n    tips:\n      title: 高級搜尋提示\n      tag: \"<1>[tag]</1> 在指定標籤中搜尋\"\n      user: \"<1>user:username</1> 根據作者搜尋\"\n      answer: \"<1>answers:0</1> 搜尋未回答的問題\"\n      score: \"<1>score:3</1> 得分為 3+ 的帖子\"\n      question: \"<1>is:question</1> 只搜尋問題\"\n      is_answer: \"<1>is:answer</1> 只搜尋回答\"\n    empty: 找不到任何相關的內容。<br /> 請嘗試其他關鍵字，或者減少查找內容的長度。\n  share:\n    name: 分享\n    copy: 複製連結\n    via: 分享在...\n    copied: 已複製\n    facebook: 分享到 Facebook\n    twitter: Share to X\n  cannot_vote_for_self: You can't vote for your own post.\n  modal_confirm:\n    title: 發生錯誤...\n  delete_permanently:\n    title: Delete permanently\n    content: Are you sure you want to delete permanently?\n  account_result:\n    success: 你的帳號已通過驗證，即將返回首頁。\n    link: 繼續訪問主頁\n    oops: Oops!\n    invalid: The link you used no longer works.\n    confirm_new_email: 你的電子郵箱已更新\n    confirm_new_email_invalid: >-\n      抱歉，此驗證連結已失效。也許是你的郵箱已經成功更改了？\n  unsubscribe:\n    page_title: 退訂\n    success_title: 取消訂閱成功\n    success_desc: 您已成功從訂閱者清單中移除且不會在收到任何來自我們的郵件。\n    link: 更改設置\n  question:\n    following_tags: 已關注的標籤\n    edit: 編輯\n    save: 儲存\n    follow_tag_tip: 按照標籤整理您的問題列表。\n    hot_questions: 熱門問題\n    all_questions: 全部問題\n    x_questions: \"{{ count }} 個問題\"\n    x_answers: \"{{ count }} 個回答\"\n    x_posts: \"{{ count }} Posts\"\n    questions: 個問題\n    answers: 回答\n    newest: 最新的\n    active: 活躍的\n    hot: Hot\n    frequent: Frequent\n    recommend: Recommend\n    score: 評分\n    unanswered: 未回答\n    modified: 修改於\n    answered: 回答於\n    asked: 提問於\n    closed: 已關閉\n    follow_a_tag: 關注一個標籤\n    more: 更多\n  personal:\n    overview: 概覽\n    answers: 回答\n    answer: 回答\n    questions: 問題\n    question: 問題\n    bookmarks: 書籤\n    reputation: 聲望\n    comments: 評論\n    votes: 得票\n    badges: Badges\n    newest: 最新\n    score: 評分\n    edit_profile: Edit profile\n    visited_x_days: \"已造訪 {{ count }} 天\"\n    viewed: 閱讀次數\n    joined: 加入於\n    comma: \",\"\n    last_login: 出現時間\n    about_me: 關於我\n    about_me_empty: \"// 你好, 世界 !\"\n    top_answers: 熱門回答\n    top_questions: 熱門問題\n    stats: 狀態\n    list_empty: 沒有找到相關的內容。<br />試試看其他標籤？\n    content_empty: No posts found.\n    accepted: 已採納\n    answered: 回答於\n    asked: 提問於\n    downvoted: downvoted\n    mod_short: MOD\n    mod_long: 管理員\n    x_reputation: 聲望\n    x_votes: 得票\n    x_answers: 個回答\n    x_questions: 個問題\n    recent_badges: Recent Badges\n  install:\n    title: Installation\n    next: 下一步\n    done: 完成\n    config_yaml_error: 無法建立 config.yaml 檔。\n    lang:\n      label: Please choose a language\n    db_type:\n      label: Database engine\n    db_username:\n      label: 用戶名\n      placeholder: 根\n      msg: 用戶名不能為空\n    db_password:\n      label: 密碼\n      placeholder: root\n      msg: 密碼不能為空\n    db_host:\n      label: Database host\n      placeholder: \"db: 3306\"\n      msg: Database host cannot be empty.\n    db_name:\n      label: Database name\n      placeholder: 回答\n      msg: Database name cannot be empty.\n    db_file:\n      label: Database file\n      placeholder: /data/answer.db\n      msg: Database file cannot be empty.\n    ssl_enabled:\n      label: Enable SSL\n    ssl_enabled_on:\n      label: On\n    ssl_enabled_off:\n      label: Off\n    ssl_mode:\n      label: SSL Mode\n    ssl_root_cert:\n      placeholder: sslrootcert file path\n      msg: Path to sslrootcert file cannot be empty\n    ssl_cert:\n      placeholder: sslcert file path\n      msg: Path to sslcert file cannot be empty\n    ssl_key:\n      placeholder: sslkey file path\n      msg: Path to sslkey file cannot be empty\n    config_yaml:\n      title: 創建 config.yaml\n      label: 已創建 config.yaml 文件。\n      desc: >-\n        您可以手動在 <1>/var/wwww/xxx/</1> 目錄中創建<1>config.yaml</1> 文件並粘貼以下文本。\n      info: 完成後點擊\"下一步\"按鈕。\n    site_information: 網站資訊\n    admin_account: 管理員帳戶\n    site_name:\n      label: Site name\n      msg: Site name cannot be empty.\n      msg_max_length: Site name must be at maximum 30 characters in length.\n    site_url:\n      label: 網站 URL\n      text: 此網站的地址。\n      msg:\n        empty: 網站URL不能為空。\n        incorrect: 網站URL格式不正確。\n        max_length: Site URL must be at maximum 512 characters in length.\n    contact_email:\n      label: Contact email\n      text: 負責本網站的主要聯絡人的電子郵件地址。\n      msg:\n        empty: Contact email cannot be empty.\n        incorrect: Contact email incorrect format.\n    login_required:\n      label: Private\n      switch: Login required\n      text: Only logged in users can access this community.\n    admin_name:\n      label: 暱稱\n      msg: 暱稱不能為空。\n      character: 'Must use the character set \"a-z\", \"0-9\", \" - . _\"'\n      msg_max_length: Name must be between 2 to 30 characters in length.\n    admin_password:\n      label: 密碼\n      text: >-\n        您需要此密碼才能登入。請將其儲存在一個安全的位置。\n      msg: 密碼不能為空。\n      msg_min_length: Password must be at least 8 characters in length.\n      msg_max_length: Password must be at maximum 32 characters in length.\n    admin_confirm_password:\n      label: \"Confirm Password\"\n      text: \"Please re-enter your password to confirm.\"\n      msg: \"Confirm password does not match.\"\n    admin_email:\n      label: 郵箱\n      text: 您需要此電子郵件才能登入。\n      msg:\n        empty: 郵箱不能為空。\n        incorrect: 郵箱格式不正確。\n    ready_title: Your site is ready\n    ready_desc: >-\n      如果你想改變更多的設定，請瀏覽<1>管理員部分</1>；在網站選單中找到它。\n    good_luck: \"玩得愉快，祝您好運！\"\n    warn_title: 警告\n    warn_desc: >-\n      檔案<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置項，請先刪除它。\n    install_now: 您可以嘗試<1>現在安裝</1>。\n    installed: 已安裝\n    installed_desc: >-\n      您似乎已經安裝過了。要重新安裝，請先清除舊的資料庫表。\n    db_failed: 資料連接異常！\n    db_failed_desc: >-\n      This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.\n  counts:\n    views: 觀看\n    votes: 得票\n    answers: 回答\n    accepted: 已採納\n  page_error:\n    http_error: HTTP 错误 {{ code }}\n    desc_403: You don't have permission to access this page.\n    desc_404: Unfortunately, this page doesn't exist.\n    desc_50X: The server encountered an error and could not complete your request.\n    back_home: Back to homepage\n  page_maintenance:\n    desc: \"我們正在維護中，很快就會回來。\"\n  nav_menus:\n    dashboard: 後台管理\n    contents: 內容\n    questions: 問題\n    answers: 回答\n    users: 使用者管理\n    badges: Badges\n    flags: 檢舉\n    settings: 設定\n    general: 一般\n    interface: 介面\n    smtp: SMTP\n    branding: 品牌\n    legal: 法律條款\n    write: 撰寫\n    terms: Terms\n    tos: 服務條款\n    privacy: 隱私政策\n    seo: SEO\n    customize: 自定義\n    themes: 主題\n    login: 登入\n    privileges: Privileges\n    plugins: Plugins\n    installed_plugins: Installed Plugins\n    apperance: Appearance\n    community: Community\n    advanced: Advanced\n    tags: Tags\n    rules: Rules\n    policies: Policies\n    security: Security\n    files: Files\n    apikeys: API Keys\n    intelligence: Intelligence\n    ai_assistant: AI Assistant\n    ai_settings: AI Settings\n    mcp: MCP\n  website_welcome: Welcome to {{site_name}}\n  user_center:\n    login: Login\n    qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n    login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n  badges:\n    modal:\n      title: Congratulations\n      content: You've earned a new badge.\n      close: Close\n      confirm: View badges\n    title: Badges\n    awarded: Awarded\n    earned_×: Earned ×{{ number }}\n    ×_awarded: \"{{ number }} awarded\"\n    can_earn_multiple: You can earn this multiple times.\n    earned: Earned\n  admin:\n    admin_header:\n      title: 後台管理\n    dashboard:\n      title: 後台管理\n      welcome: Welcome to Admin!\n      site_statistics: Site statistics\n      questions: \"問題:\"\n      resolved: \"Resolved:\"\n      unanswered: \"Unanswered:\"\n      answers: \"回答:\"\n      comments: \"評論:\"\n      votes: \"投票:\"\n      users: \"Users:\"\n      flags: \"檢舉:\"\n      reviews: \"Reviews:\"\n      site_health: Site health\n      version: \"版本\"\n      https: \"HTTPS:\"\n      upload_folder: \"Upload folder:\"\n      run_mode: \"Running mode:\"\n      private: Private\n      public: Public\n      smtp: \"SMTP:\"\n      timezone: \"時區：\"\n      system_info: System info\n      go_version: \"Go version:\"\n      database: \"Database:\"\n      database_size: \"Database size:\"\n      storage_used: \"已用儲存空間：\"\n      uptime: \"運行時間：\"\n      links: Links\n      plugins: Plugins\n      github: GitHub\n      blog: Blog\n      contact: Contact\n      forum: Forum\n      documents: 文件\n      feedback: 用戶反饋\n      support: 支持\n      review: 審核\n      config: 配置\n      update_to: 更新到\n      latest: 最新版本\n      check_failed: 校驗失敗\n      \"yes\": \"是\"\n      \"no\": \"否\"\n      not_allowed: 不允許\n      allowed: 允許\n      enabled: 已啟用\n      disabled: 停用\n      writable: Writable\n      not_writable: Not writable\n    flags:\n      title: 檢舉\n      pending: 等待處理\n      completed: 已完成\n      flagged: 已標記\n      flagged_type: Flagged {{ type }}\n      created: 創建於\n      action: 操作\n      review: 審核\n    user_role_modal:\n      title: 更改用戶狀態為...\n      btn_cancel: 取消\n      btn_submit: 提交\n    new_password_modal:\n      title: Set new password\n      form:\n        fields:\n          password:\n            label: Password\n            text: The user will be logged out and need to login again.\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    edit_profile_modal:\n      title: Edit profile\n      form:\n        fields:\n          display_name:\n            label: Display name\n            msg_range: Display name must be 2-30 characters in length.\n          username:\n            label: Username\n            msg_range: Username must be 2-30 characters in length.\n          email:\n            label: Email\n            msg_invalid: Invalid Email Address.\n      edit_success: Edited successfully\n      btn_cancel: Cancel\n      btn_submit: Submit\n    user_modal:\n      title: Add new user\n      form:\n        fields:\n          users:\n            label: Bulk add user\n            placeholder: \"John Smith, john@example.com, BUSYopr2\\nAlice, alice@example.com, fpDntV8q\"\n            text: Separate “name, email, password” with commas. One user per line.\n            msg: \"Please enter the user's email, one per line.\"\n          display_name:\n            label: Display name\n            msg: Display name must be 2-30 characters in length.\n          email:\n            label: Email\n            msg: Email is not valid.\n          password:\n            label: Password\n            msg: Password must be at 8-32 characters in length.\n      btn_cancel: Cancel\n      btn_submit: Submit\n    users:\n      title: 用戶\n      name: 名稱\n      email: 郵箱\n      reputation: 聲望\n      created_at: Created time\n      delete_at: Deleted time\n      suspend_at: Suspended time\n      suspend_until: Suspend until\n      status: 狀態\n      role: 角色\n      action: 操作\n      change: 更改\n      all: 全部\n      staff: 工作人員\n      more: More\n      inactive: 不活躍\n      suspended: 已停權\n      deleted: 已刪除\n      normal: 正常\n      Moderator: 版主\n      Admin: 管理員\n      User: 用戶\n      filter:\n        placeholder: \"按名稱篩選，用戶：id\"\n      set_new_password: 設置新密碼\n      edit_profile: Edit profile\n      change_status: 更改狀態\n      change_role: 更改角色\n      show_logs: 顯示日誌\n      add_user: 新增使用者\n      deactivate_user:\n        title: Deactivate user\n        content: An inactive user must re-validate their email.\n      delete_user:\n        title: Delete this user\n        content: Are you sure you want to delete this user? This is permanent!\n        remove: Remove their content\n        label: Remove all questions, answers, comments, etc.\n        text: Don’t check this if you wish to only delete the user’s account.\n      suspend_user:\n        title: Suspend this user\n        content: A suspended user can't log in.\n        label: How long will the user be suspended for?\n        forever: Forever\n    questions:\n      page_title: 問題\n      unlisted: Unlisted\n      post: 標題\n      votes: 得票數\n      answers: 回答\n      created: 創建於\n      status: 狀態\n      action: 操作\n      change: 更改\n      pending: Pending\n      filter:\n        placeholder: \"按標題過濾，問題:id\"\n    answers:\n      page_title: 回答\n      post: 發布\n      votes: 得票數\n      created: 創建於\n      status: 狀態\n      action: 操作\n      change: 更改\n      filter:\n        placeholder: \"按名稱篩選，answer:id\"\n    general:\n      page_title: 一般\n      name:\n        label: Site name\n        msg: 不能為空\n        text: \"網站的名稱，如標題標籤中所用。\"\n      site_url:\n        label: 網站網址\n        msg: 網站網址不能為空。\n        validate: 請輸入一個有效的 URL。\n        text: 此網站的網址。\n      short_desc:\n        label: Short site description\n        msg: 網站簡短描述不能為空。\n        text: \"簡短的描述，如主頁上的標題標籤所使用的那样。\"\n      desc:\n        label: Site description\n        msg: 網站描述不能為空。\n        text: \"使用一句話描述本站，作為網站的描述（Html 的 meta 標籤）。\"\n      contact_email:\n        label: Contact email\n        msg: 聯絡人信箱不能為空。\n        validate: 聯絡人信箱無效。\n        text: 負責本網站的主要聯絡人的電子郵件信箱。\n      check_update:\n        label: Software updates\n        text: Automatically check for updates\n    interface:\n      page_title: 介面\n      language:\n        label: Interface language\n        msg: 界面語言不能為空\n        text: 設置用戶界面語言，在刷新頁面后生效。\n      time_zone:\n        label: 時區\n        msg: 時區不能為空。\n        text: 選擇一個與您相同時區的城市。\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar base URL\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n    smtp:\n      page_title: SMTP\n      from_email:\n        label: From email\n        msg: 發件人電子郵件不能为空。\n        text: 發送郵件的郵箱地址\n      from_name:\n        label: From name\n        msg: 發件人名稱不能为空。\n        text: 發件人的名稱\n      smtp_host:\n        label: SMTP host\n        msg: SMTP 主機名稱不能為空。\n        text: 郵件服務器\n      encryption:\n        label: 加密\n        msg: 加密不能為空。\n        text: 對於大多數服務器，SSL 是推薦的選項。\n        ssl: SSL\n        tls: TLS\n        none: 無\n      smtp_port:\n        label: SMTP port\n        msg: SMTP 埠必須在 1 ~ 65535 之間。\n        text: 郵件服務器的端口號。\n      smtp_username:\n        label: SMTP username\n        msg: SMTP 用戶名不能為空。\n      smtp_password:\n        label: SMTP password\n        msg: SMTP 密碼不能為空。\n      test_email_recipient:\n        label: Test email recipients\n        text: 提供用於接收測試郵件的郵箱地址。\n        msg: 測試郵件收件人無效\n      smtp_authentication:\n        label: 啟用身份驗證\n        title: SMTP authentication\n        msg: SMTP 身份驗證不能為空。\n        \"yes\": \"是\"\n        \"no\": \"否\"\n    branding:\n      page_title: 品牌\n      logo:\n        label: 標誌\n        msg: 圖標不能為空。\n        text: 在你的網站左上方的Logo圖標。使用一個高度為56，長寬比大於3:1的寬長方形圖像。如果留空，將顯示網站標題文本。\n      mobile_logo:\n        label: Mobile logo\n        text: 在您網站的移動版本上使用的徽標。 使用高度為 56 的寬矩形圖像。如果留空，將使用“徽標”設置中的圖像。\n      square_icon:\n        label: Square icon\n        msg: 方形圖示不能為空。\n        text: 用作元數據圖標的基礎的圖像。最好是大於512x512。\n      favicon:\n        label: 網站圖示\n        text: 您網站的圖標。 要在 CDN 上正常工作，它必須是 png。 將調整為 32x32的大小。 如果留空，將使用“方形圖標”。\n    legal:\n      page_title: 法律條款\n      terms_of_service:\n        label: Terms of service\n        text: \"您可以在此加入服務內容的條款。如果您已經在別處托管了文檔，請在這裡提供完整的URL。\"\n      privacy_policy:\n        label: Privacy policy\n        text: \"您可以在此加入隱私政策內容。如果您已經在別處托管了文檔，請在這裡提供完整的URL。\"\n      external_content_display:\n        label: External content\n        text: \"Content includes images, videos, and media embedded from external websites.\"\n        always_display: Always display external content\n        ask_before_display: Ask before displaying external content\n    write:\n      page_title: Files\n      min_content:\n        label: Minimum question body length\n        text: Minimum allowed question body length in characters.\n      restrict_answer:\n        title: Answer write\n        label: Each user can only write one answer for each question\n        text: \"Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused.\"\n      min_tags:\n        label: \"Minimum tags per question\"\n        text: \"Minimum number of tags required in a question.\"\n      recommend_tags:\n        label: Recommend tags\n        text: \"Recommend tags will show in the dropdown list by default.\"\n        msg:\n          contain_reserved: \"recommended tags cannot contain reserved tags\"\n      required_tag:\n        title: Set required tags\n        label: Set “Recommend tags” as required tags\n        text: \"每個新問題必須至少有一個推薦標籤。\"\n      reserved_tags:\n        label: Reserved tags\n        text: \"Reserved tags can only be used by moderator.\"\n      image_size:\n        label: Max image size (MB)\n        text: \"The maximum image upload size.\"\n      attachment_size:\n        label: Max attachment size (MB)\n        text: \"The maximum attachment files upload size.\"\n      image_megapixels:\n        label: Max image megapixels\n        text: \"Maximum number of megapixels allowed for an image.\"\n      image_extensions:\n        label: Authorized image extensions\n        text: \"A list of file extensions allowed for image display, separate with commas.\"\n      attachment_extensions:\n        label: Authorized attachment extensions\n        text: \"A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues.\"\n    seo:\n      page_title: 搜尋引擎優化\n      permalink:\n        label: 固定連結\n        text: 自定義URL結構可以提高可用性，以及你的連結的向前相容性。\n      robots:\n        label: robots.txt\n        text: 這將永久覆蓋任何相關的網站設置。\n    themes:\n      page_title: 主題\n      themes:\n        label: 主題\n        text: 選擇一個現有主題。\n      color_scheme:\n        label: Color scheme\n      navbar_style:\n        label: Navbar background style\n      primary_color:\n        label: 主色調\n        text: 修改您主題使用的顏色\n      layout:\n        label: Layout\n        full_width: Full-width\n        fixed_width: Fixed-width\n    css_and_html:\n      page_title: CSS 與 HTML\n      custom_css:\n        label: 自定義CSS\n        text: >\n\n      head:\n        label: 頭部\n        text: >\n\n      header:\n        label: 標題\n        text: >\n\n      footer:\n        label: 頁尾\n        text: This will insert before &lt;/body>.\n      sidebar:\n        label: Sidebar\n        text: This will insert in sidebar.\n    login:\n      page_title: 登入\n      membership:\n        title: 會員\n        label: 允許新註冊\n        text: 關閉以防止任何人創建新帳戶。\n      email_registration:\n        title: Email registration\n        label: Allow email registration\n        text: Turn off to prevent anyone creating new account through email.\n      allowed_email_domains:\n        title: Allowed email domains\n        text: Email domains that users must register accounts with. One domain per line. Ignored when empty.\n      private:\n        title: 非公開的\n        label: 需要登入\n        text: 只有登入使用者才能訪問這個社群。\n      password_login:\n        title: Password login\n        label: Allow email and password login\n        text: \"WARNING: If turn off, you may be unable to log in if you have not previously configured other login method.\"\n    installed_plugins:\n      title: Installed Plugins\n      plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository</1>.\n      filter:\n        all: All\n        active: Active\n        inactive: Inactive\n        outdated: Outdated\n      plugins:\n        label: Plugins\n        text: Select an existing plugin.\n      name: Name\n      version: Version\n      status: Status\n      action: Action\n      deactivate: Deactivate\n      activate: Activate\n      settings: Settings\n    settings_users:\n      title: Users\n      avatar:\n        label: Default avatar\n        text: For users without a custom avatar of their own.\n      gravatar_base_url:\n        label: Gravatar 基礎網址\n        text: URL of the Gravatar provider's API base. Ignored when empty.\n      profile_editable:\n        title: Profile editable\n      allow_update_display_name:\n        label: Allow users to change their display name\n      allow_update_username:\n        label: Allow users to change their username\n      allow_update_avatar:\n        label: Allow users to change their profile image\n      allow_update_bio:\n        label: Allow users to change their about me\n      allow_update_website:\n        label: Allow users to change their website\n      allow_update_location:\n        label: Allow users to change their location\n    privilege:\n      title: Privileges\n      level:\n        label: Reputation required level\n        text: Choose the reputation required for the privileges\n      msg:\n        should_be_number: the input should be number\n        number_larger_1: number should be equal or larger than 1\n    badges:\n      action: Action\n      active: Active\n      activate: Activate\n      all: All\n      awards: Awards\n      deactivate: Deactivate\n      filter:\n        placeholder: Filter by name, badge:id\n      group: Group\n      inactive: Inactive\n      name: Name\n      show_logs: Show logs\n      status: Status\n      title: Badges\n    apikeys:\n      title: API Keys\n      add_api_key: Add API Key\n      desc: Description\n      scope: Scope\n      key: Key\n      created: Created\n      last_used: Last used\n      add_or_edit_modal:\n        add_title: Add API Key\n        edit_title: Edit API Key\n        description: Description\n        description_required: Description is required.\n        scope: Scope\n        global: Global\n        read-only: Read-only\n      created_modal:\n        title: API key created\n        api_key: API key\n        description: This key will not be displayed again. Make sure you take a copy before continuing.\n      delete_modal:\n        title: Delete API Key\n        content: Any applications or scripts using this key will no longer be able to access the API. This is permanent!\n    ai_settings:\n      enabled:\n        label: AI enabled\n        check: Enable AI features\n        text: The AI model must be configured correctly before it can be used.\n      provider:\n        label: Provider\n      api_host:\n        label: API host\n        msg: API host is required\n      api_key:\n        label: API key\n        check: Check\n        check_success: \"Connection successful.\"\n        msg: API key is required\n      model:\n        label: Model\n        msg: Model is required\n      add_success: AI settings updated successfully.\n    conversations:\n      topic: Topic\n      helpful: Helpful\n      unhelpful: Unhelpful\n      created: Created\n      action: Action\n      empty: No conversations found.\n      delete_modal:\n        title: Delete conversation\n        content: Are you sure you want to delete this conversation? This is permanent!\n        delete_success: Conversation deleted successfully.\n    mcp:\n      mcp_server:\n        label: MCP server\n        switch: Enabled\n      type:\n        label: Type\n      url:\n        label: URL\n      http_header:\n        label: HTTP header\n        text: Please replace {key} with the API Key.\n  form:\n    optional: (選填)\n    empty: 不能為空\n    invalid: 是無效的\n    btn_submit: 儲存\n    not_found_props: \"所需屬性 {{ key }} 未找到。\"\n    select: Select\n  page_review:\n    review: 審核\n    proposed: 提案\n    question_edit: 問題編輯\n    answer_edit: 回答編輯\n    tag_edit: '標籤管理: 編輯標籤'\n    edit_summary: 編輯摘要\n    edit_question: 編輯問題\n    edit_answer: 編輯回答\n    edit_tag: 編輯標籤\n    empty: 沒有剩餘的審核任務。\n    approve_revision_tip: Do you approve this revision?\n    approve_flag_tip: Do you approve this flag?\n    approve_post_tip: Do you approve this post?\n    approve_user_tip: Do you approve this user?\n    suggest_edits: Suggested edits\n    flag_post: Flag post\n    flag_user: Flag user\n    queued_post: Queued post\n    queued_user: Queued user\n    filter_label: Type\n    reputation: reputation\n    flag_post_type: Flagged this post as {{ type }}.\n    flag_user_type: Flagged this user as {{ type }}.\n    edit_post: Edit post\n    list_post: List post\n    unlist_post: Unlist post\n  timeline:\n    undeleted: 未刪除的\n    deleted: 刪除\n    downvote: 反對\n    upvote: 贊同\n    accept: 採納\n    cancelled: 已取消\n    commented: '評論:'\n    rollback: 回滾\n    edited: 最後編輯於\n    answered: 回答於\n    asked: 提問於\n    closed: 關閉\n    reopened: 重新開啟\n    created: 創建於\n    pin: pinned\n    unpin: unpinned\n    show: listed\n    hide: unlisted\n    title: \"歷史記錄\"\n    tag_title: \"時間線\"\n    show_votes: \"顯示投票\"\n    n_or_a: N/A\n    title_for_question: \"時間線\"\n    title_for_answer: \"{{ title }} 的 {{ author }} 回答時間線\"\n    title_for_tag: \"標籤的時間線\"\n    datetime: 日期時間\n    type: 類型\n    by: 由\n    comment: 評論\n    no_data: \"我們找不到任何東西。\"\n  users:\n    title: 用戶\n    users_with_the_most_reputation: Users with the highest reputation scores this week\n    users_with_the_most_vote: Users who voted the most this week\n    staffs: 我們的社區工作人員\n    reputation: 聲望值\n    votes: 選票\n  prompt:\n    leave_page: 你確定要離開此頁面？\n    changes_not_save: 你所做的變更可能不會儲存。\n  draft:\n    discard_confirm: Are you sure you want to discard your draft?\n  messages:\n    post_deleted: This post has been deleted.\n    post_cancel_deleted: This post has been undeleted.\n    post_pin: This post has been pinned.\n    post_unpin: This post has been unpinned.\n    post_hide_list: This post has been hidden from list.\n    post_show_list: This post has been shown to list.\n    post_reopen: This post has been reopened.\n    post_list: This post has been listed.\n    post_unlist: This post has been unlisted.\n    post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.\n    post_closed: This post has been closed.\n    answer_deleted: This answer has been deleted.\n    answer_cancel_deleted: This answer has been undeleted.\n    change_user_role: This user's role has been changed.\n    user_inactive: This user is already inactive.\n    user_normal: This user is already normal.\n    user_suspended: This user has been suspended.\n    user_deleted: This user has been deleted.\n    user_added: User has been added successfully.\n    badge_activated: This badge has been activated.\n    badge_inactivated: This badge has been inactivated.\n    users_deleted: These users have been deleted.\n    posts_deleted: These questions have been deleted.\n    answers_deleted: These answers have been deleted.\n    copy: Copy to clipboard\n    copied: Copied\n    external_content_warning: External images/media are not displayed.\n\n\n"
  },
  {
    "path": "internal/base/conf/conf.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage conf\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/base/server\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/router\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/pkg/writer\"\n\t\"github.com/segmentfault/pacman/contrib/conf/viper\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// AllConfig all config\ntype AllConfig struct {\n\tDebug         bool                          `json:\"debug\" mapstructure:\"debug\" yaml:\"debug\"`\n\tServer        *Server                       `json:\"server\" mapstructure:\"server\" yaml:\"server\"`\n\tData          *Data                         `json:\"data\" mapstructure:\"data\" yaml:\"data\"`\n\tI18n          *translator.I18n              `json:\"i18n\" mapstructure:\"i18n\" yaml:\"i18n\"`\n\tServiceConfig *service_config.ServiceConfig `json:\"service_config\" mapstructure:\"service_config\" yaml:\"service_config\"`\n\tSwaggerui     *router.SwaggerConfig         `json:\"swaggerui\" mapstructure:\"swaggerui\" yaml:\"swaggerui\"`\n\tUI            *server.UI                    `json:\"ui\" mapstructure:\"ui\" yaml:\"ui\"`\n}\n\ntype envConfigOverrides struct {\n\tSwaggerHost        string\n\tSwaggerAddressPort string\n\tSiteAddr           string\n}\n\nfunc loadEnvs() (envOverrides *envConfigOverrides) {\n\treturn &envConfigOverrides{\n\t\tSwaggerHost:        os.Getenv(\"SWAGGER_HOST\"),\n\t\tSwaggerAddressPort: os.Getenv(\"SWAGGER_ADDRESS_PORT\"),\n\t\tSiteAddr:           os.Getenv(\"SITE_ADDR\"),\n\t}\n}\n\ntype PathIgnore struct {\n\tUsers []string `yaml:\"users\"`\n}\n\n// Server server config\ntype Server struct {\n\tHTTP *server.HTTP `json:\"http\" mapstructure:\"http\" yaml:\"http\"`\n}\n\n// Data data config\ntype Data struct {\n\tDatabase *data.Database  `json:\"database\" mapstructure:\"database\" yaml:\"database\"`\n\tCache    *data.CacheConf `json:\"cache\" mapstructure:\"cache\" yaml:\"cache\"`\n}\n\n// SetDefault set default config\nfunc (c *AllConfig) SetDefault() {\n\tif c.UI == nil {\n\t\tc.UI = &server.UI{}\n\t}\n}\n\nfunc (c *AllConfig) SetEnvironmentOverrides() {\n\tenvs := loadEnvs()\n\tif envs.SiteAddr != \"\" {\n\t\tc.Server.HTTP.Addr = envs.SiteAddr\n\t}\n\tif envs.SwaggerHost != \"\" {\n\t\tc.Swaggerui.Host = envs.SwaggerHost\n\t}\n\tif envs.SwaggerAddressPort != \"\" {\n\t\tc.Swaggerui.Address = envs.SwaggerAddressPort\n\t}\n}\n\n// ReadConfig read config\nfunc ReadConfig(configFilePath string) (c *AllConfig, err error) {\n\tif len(configFilePath) == 0 {\n\t\tconfigFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName)\n\t}\n\tc = &AllConfig{}\n\tconfig, err := viper.NewWithPath(configFilePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = config.Parse(&c); err != nil {\n\t\treturn nil, err\n\t}\n\tc.SetDefault()\n\tc.SetEnvironmentOverrides()\n\treturn c, nil\n}\n\n// RewriteConfig rewrite config file path\nfunc RewriteConfig(configFilePath string, allConfig *AllConfig) error {\n\tbuf := bytes.Buffer{}\n\tenc := yaml.NewEncoder(&buf)\n\tdefer func() {\n\t\t_ = enc.Close()\n\t}()\n\tenc.SetIndent(2)\n\tif err := enc.Encode(allConfig); err != nil {\n\t\treturn err\n\t}\n\treturn writer.ReplaceFile(configFilePath, buf.String())\n}\n"
  },
  {
    "path": "internal/base/constant/acticity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\ntype ActivityTypeKey string\n\nconst (\n\tActEdited    = \"edited\"\n\tActClosed    = \"closed\"\n\tActVotedDown = \"voted_down\"\n\tActVotedUp   = \"voted_up\"\n\tActVoteDown  = \"vote_down\"\n\tActVoteUp    = \"vote_up\"\n\tActUpVote    = \"upvote\"\n\tActDownVote  = \"downvote\"\n\tActFollow    = \"follow\"\n\tActAccepted  = \"accepted\"\n\tActAccept    = \"accept\"\n\tActPin       = \"pin\"\n\tActUnPin     = \"unpin\"\n\tActShow      = \"show\"\n\tActHide      = \"hide\"\n)\n\nconst (\n\tActQuestionAsked     ActivityTypeKey = \"question.asked\"\n\tActQuestionClosed    ActivityTypeKey = \"question.closed\"\n\tActQuestionReopened  ActivityTypeKey = \"question.reopened\"\n\tActQuestionAnswered  ActivityTypeKey = \"question.answered\"\n\tActQuestionCommented ActivityTypeKey = \"question.commented\"\n\tActQuestionAccept    ActivityTypeKey = \"question.accept\"\n\tActQuestionUpvote    ActivityTypeKey = \"question.upvote\"\n\tActQuestionDownVote  ActivityTypeKey = \"question.downvote\"\n\tActQuestionEdited    ActivityTypeKey = \"question.edited\"\n\tActQuestionRollback  ActivityTypeKey = \"question.rollback\"\n\tActQuestionDeleted   ActivityTypeKey = \"question.deleted\"\n\tActQuestionUndeleted ActivityTypeKey = \"question.undeleted\"\n\tActQuestionPin       ActivityTypeKey = \"question.pin\"\n\tActQuestionUnPin     ActivityTypeKey = \"question.unpin\"\n\tActQuestionHide      ActivityTypeKey = \"question.hide\"\n\tActQuestionShow      ActivityTypeKey = \"question.show\"\n)\n\nconst (\n\tActAnswerAnswered  ActivityTypeKey = \"answer.answered\"\n\tActAnswerCommented ActivityTypeKey = \"answer.commented\"\n\tActAnswerAccept    ActivityTypeKey = \"answer.accept\"\n\tActAnswerUpvote    ActivityTypeKey = \"answer.upvote\"\n\tActAnswerDownVote  ActivityTypeKey = \"answer.downvote\"\n\tActAnswerEdited    ActivityTypeKey = \"answer.edited\"\n\tActAnswerRollback  ActivityTypeKey = \"answer.rollback\"\n\tActAnswerDeleted   ActivityTypeKey = \"answer.deleted\"\n\tActAnswerUndeleted ActivityTypeKey = \"answer.undeleted\"\n)\n\nconst (\n\tActTagCreated   ActivityTypeKey = \"tag.created\"\n\tActTagEdited    ActivityTypeKey = \"tag.edited\"\n\tActTagRollback  ActivityTypeKey = \"tag.rollback\"\n\tActTagDeleted   ActivityTypeKey = \"tag.deleted\"\n\tActTagUndeleted ActivityTypeKey = \"tag.undeleted\"\n)\n"
  },
  {
    "path": "internal/base/constant/ai_config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tAIConfigProvider = \"ai_config.provider\"\n)\n\nconst (\n\tDefaultAIPromptConfigZhCN = `你是一个智能助手，可以帮助用户查询系统中的信息。用户问题：%s\n\n你可以使用以下工具来查询系统信息：\n- get_questions: 搜索系统中已存在的问题，使用这个工具可以获取问题列表后注意需要使用 get_answers_by_question_id 获取问题的答案\n- get_answers_by_question_id: 根据问题ID获取该问题的所有答案\n- get_comments: 搜索评论信息\n- get_tags: 搜索标签信息\n- get_tag_detail: 获取特定标签的详细信息\n- get_user: 搜索用户信息\n\n请根据用户的问题智能地使用这些工具来提供准确的答案。如果需要查询系统信息，请先使用相应的工具获取数据。`\n\tDefaultAIPromptConfigEnUS = `You are an intelligent assistant that can help users query information in the system. User question: %s\n\nYou can use the following tools to query system information:\n- get_questions: Search for existing questions in the system. After using this tool to get the question list, you need to use get_answers_by_question_id to get the answers to the questions\n- get_answers_by_question_id: Get all answers for a question based on question ID\n- get_comments: Search for comment information\n- get_tags: Search for tag information\n- get_tag_detail: Get detailed information about a specific tag\n- get_user: Search for user information\n\nPlease intelligently use these tools based on the user's question to provide accurate answers. If you need to query system information, please use the appropriate tools to get the data first.`\n)\n"
  },
  {
    "path": "internal/base/constant/cache_key.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nimport \"time\"\n\nconst (\n\tUserStatusChangedCacheKey                  = \"answer:user:status:\"\n\tUserStatusChangedCacheTime                 = 7 * 24 * time.Hour\n\tUserTokenCacheKey                          = \"answer:user:token:\"\n\tUserTokenCacheTime                         = 7 * 24 * time.Hour\n\tUserVisitTokenCacheKey                     = \"answer:user:visit:\"\n\tUserVisitCacheTime                         = 7 * 24 * 60 * 60\n\tUserVisitCookiesCacheKey                   = \"visit\"\n\tAdminTokenCacheKey                         = \"answer:admin:token:\"\n\tAdminTokenCacheTime                        = 7 * 24 * time.Hour\n\tUserTokenMappingCacheKey                   = \"answer:user-token:mapping:\"\n\tUserEmailCodeCacheKey                      = \"answer:user:email-code:\"\n\tUserEmailCodeCacheTime                     = 10 * time.Minute\n\tUserLatestEmailCodeCacheKey                = \"answer:user-id:email-code:\"\n\tSiteInfoCacheKey                           = \"answer:site-info:\"\n\tSiteInfoCacheTime                          = 1 * time.Hour\n\tConfigID2KEYCacheKeyPrefix                 = \"answer:config:id:\"\n\tConfigKEY2ContentCacheKeyPrefix            = \"answer:config:key:\"\n\tConfigCacheTime                            = 1 * time.Hour\n\tConnectorUserExternalInfoCacheKey          = \"answer:connector:\"\n\tConnectorUserExternalInfoCacheTime         = 10 * time.Minute\n\tSiteMapQuestionCacheKeyPrefix              = \"answer:sitemap:question:%d\"\n\tSiteMapQuestionCacheTime                   = time.Hour\n\tSitemapMaxSize                             = 50000\n\tNewQuestionNotificationLimitCacheKeyPrefix = \"answer:new-question-notification-limit:\"\n\tNewQuestionNotificationLimitCacheTime      = 7 * 24 * time.Hour\n\tNewQuestionNotificationLimitMax            = 50\n\tRateLimitCacheKeyPrefix                    = \"answer:rate-limit:\"\n\tRateLimitCacheTime                         = 5 * time.Minute\n\tRedDotCacheKey                             = \"answer:red-dot:%s:%s\"\n\tRedDotCacheTime                            = 30 * 24 * time.Hour\n)\n"
  },
  {
    "path": "internal/base/constant/comment.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nimport \"time\"\n\nconst (\n\tCommentEditDeadline = time.Minute * 5\n)\n"
  },
  {
    "path": "internal/base/constant/constant.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tDefaultPageSize = 20 // Default number of pages\n\tDefaultBulkUser = 5000\n)\n\nvar (\n\tVersion   = \"\"\n\tRevision  = \"\"\n\tGoVersion = \"\"\n)\n\nvar Timezones = []string{\n\t// Americas\n\t\"America/New_York\",\n\t\"America/Chicago\",\n\t\"America/Los_Angeles\",\n\t\"America/Toronto\",\n\t\"America/Vancouver\",\n\t\"America/Mexico_City\",\n\t\"America/Sao_Paulo\",\n\t\"America/Buenos_Aires\",\n\n\t// Europe\n\t\"Europe/London\",\n\t\"Europe/Paris\",\n\t\"Europe/Berlin\",\n\t\"Europe/Madrid\",\n\t\"Europe/Rome\",\n\t\"Europe/Moscow\",\n\n\t// Asia\n\t\"Asia/Shanghai\",\n\t\"Asia/Tokyo\",\n\t\"Asia/Singapore\",\n\t\"Asia/Dubai\",\n\t\"Asia/Hong_Kong\",\n\t\"Asia/Seoul\",\n\t\"Asia/Bangkok\",\n\t\"Asia/Kolkata\",\n\n\t// Pacific\n\t\"Australia/Sydney\",\n\t\"Australia/Melbourne\",\n\t\"Pacific/Auckland\",\n\n\t// Africa\n\t\"Africa/Cairo\",\n\t\"Africa/Johannesburg\",\n\t\"Africa/Lagos\",\n}\n"
  },
  {
    "path": "internal/base/constant/ctx_flag.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tAcceptLanguageFlag = \"Accept-Language\"\n\tShortIDFlag        = \"Short-ID-Enabled\"\n)\n\ntype ContextKey string\n\nconst (\n\tAcceptLanguageContextKey ContextKey = ContextKey(AcceptLanguageFlag)\n\tShortIDContextKey        ContextKey = ContextKey(ShortIDFlag)\n)\n"
  },
  {
    "path": "internal/base/constant/email_tpl_key.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tEmailTplKeyChangeEmailTitle = \"email_tpl.change_email.title\"\n\tEmailTplKeyChangeEmailBody  = \"email_tpl.change_email.body\"\n\n\tEmailTplKeyNewAnswerTitle = \"email_tpl.new_answer.title\"\n\tEmailTplKeyNewAnswerBody  = \"email_tpl.new_answer.body\"\n\n\tEmailTplKeyNewCommentTitle = \"email_tpl.new_comment.title\"\n\tEmailTplKeyNewCommentBody  = \"email_tpl.new_comment.body\"\n\n\tEmailTplKeyPassResetTitle = \"email_tpl.pass_reset.title\"\n\tEmailTplKeyPassResetBody  = \"email_tpl.pass_reset.body\"\n\n\tEmailTplKeyRegisterTitle = \"email_tpl.register.title\"\n\tEmailTplKeyRegisterBody  = \"email_tpl.register.body\"\n\n\tEmailTplKeyTestTitle = \"email_tpl.test.title\"\n\tEmailTplKeyTestBody  = \"email_tpl.test.body\"\n\n\tEmailTplKeyInvitedAnswerTitle = \"email_tpl.invited_you_to_answer.title\"\n\tEmailTplKeyInvitedAnswerBody  = \"email_tpl.invited_you_to_answer.body\"\n\n\tEmailTplKeyNewQuestionTitle = \"email_tpl.new_question.title\"\n\tEmailTplKeyNewQuestionBody  = \"email_tpl.new_question.body\"\n)\n"
  },
  {
    "path": "internal/base/constant/event.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\n// EventType event type. It is used to define the type of event. Such as object.action\ntype EventType string\n\n// event object\nconst (\n\teventQuestion = \"question\"\n\teventAnswer   = \"answer\"\n\teventComment  = \"comment\"\n\teventUser     = \"user\"\n)\n\n// event action\nconst (\n\teventCreate = \"create\"\n\teventUpdate = \"update\"\n\teventDelete = \"delete\"\n\teventVote   = \"vote\"\n\teventAccept = \"accept\" // only question have the accept event\n\teventShare  = \"share\"  // the object share link has been clicked\n\teventFlag   = \"flag\"\n\teventReact  = \"react\"\n)\n\nconst (\n\tEventUserUpdate EventType = eventUser + \".\" + eventUpdate\n\tEventUserShare  EventType = eventUser + \".\" + eventShare\n)\n\nconst (\n\tEventQuestionCreate EventType = eventQuestion + \".\" + eventCreate\n\tEventQuestionUpdate EventType = eventQuestion + \".\" + eventUpdate\n\tEventQuestionDelete EventType = eventQuestion + \".\" + eventDelete\n\tEventQuestionVote   EventType = eventQuestion + \".\" + eventVote\n\tEventQuestionAccept EventType = eventQuestion + \".\" + eventAccept\n\tEventQuestionFlag   EventType = eventQuestion + \".\" + eventFlag\n\tEventQuestionReact  EventType = eventQuestion + \".\" + eventReact\n)\n\nconst (\n\tEventAnswerCreate EventType = eventAnswer + \".\" + eventCreate\n\tEventAnswerUpdate EventType = eventAnswer + \".\" + eventUpdate\n\tEventAnswerDelete EventType = eventAnswer + \".\" + eventDelete\n\tEventAnswerVote   EventType = eventAnswer + \".\" + eventVote\n\tEventAnswerFlag   EventType = eventAnswer + \".\" + eventFlag\n\tEventAnswerReact  EventType = eventAnswer + \".\" + eventReact\n)\n\nconst (\n\tEventCommentCreate EventType = eventComment + \".\" + eventCreate\n\tEventCommentUpdate EventType = eventComment + \".\" + eventUpdate\n\tEventCommentDelete EventType = eventComment + \".\" + eventDelete\n\tEventCommentVote   EventType = eventComment + \".\" + eventVote\n\tEventCommentFlag   EventType = eventComment + \".\" + eventFlag\n)\n"
  },
  {
    "path": "internal/base/constant/meta.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tReactionTooltipLabel = \"reaction.tooltip\"\n)\n"
  },
  {
    "path": "internal/base/constant/notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\t// NotificationUpdateQuestion update question\n\tNotificationUpdateQuestion = \"notification.action.update_question\"\n\t// NotificationAnswerTheQuestion answer the question\n\tNotificationAnswerTheQuestion = \"notification.action.answer_the_question\"\n\t// NotificationUpVotedTheQuestion up voted the question\n\tNotificationUpVotedTheQuestion = \"notification.action.up_voted_question\"\n\t// NotificationDownVotedTheQuestion down voted the question\n\tNotificationDownVotedTheQuestion = \"notification.action.down_voted_question\"\n\t// NotificationUpdateAnswer update answer\n\tNotificationUpdateAnswer = \"notification.action.update_answer\"\n\t// NotificationAcceptAnswer accept answer\n\tNotificationAcceptAnswer = \"notification.action.accept_answer\"\n\t// NotificationUpVotedTheAnswer up voted the answer\n\tNotificationUpVotedTheAnswer = \"notification.action.up_voted_answer\"\n\t// NotificationDownVotedTheAnswer down voted the answer\n\tNotificationDownVotedTheAnswer = \"notification.action.down_voted_answer\"\n\t// NotificationCommentQuestion comment question\n\tNotificationCommentQuestion = \"notification.action.comment_question\"\n\t// NotificationCommentAnswer comment answer\n\tNotificationCommentAnswer = \"notification.action.comment_answer\"\n\t// NotificationUpVotedTheComment up voted the comment\n\tNotificationUpVotedTheComment = \"notification.action.up_voted_comment\"\n\t// NotificationReplyToYou reply to you\n\tNotificationReplyToYou = \"notification.action.reply_to_you\"\n\t// NotificationMentionYou mention you\n\tNotificationMentionYou = \"notification.action.mention_you\"\n\t// NotificationYourQuestionIsClosed your question is closed\n\tNotificationYourQuestionIsClosed = \"notification.action.your_question_is_closed\"\n\t// NotificationYourQuestionWasDeleted your question was deleted\n\tNotificationYourQuestionWasDeleted = \"notification.action.your_question_was_deleted\"\n\t// NotificationYourAnswerWasDeleted your answer was deleted\n\tNotificationYourAnswerWasDeleted = \"notification.action.your_answer_was_deleted\"\n\t// NotificationYourCommentWasDeleted your comment was deleted\n\tNotificationYourCommentWasDeleted = \"notification.action.your_comment_was_deleted\"\n\t// NotificationInvitedYouToAnswer invited you to answer\n\tNotificationInvitedYouToAnswer = \"notification.action.invited_you_to_answer\"\n\t// NotificationEarnedBadge earned badge\n\tNotificationEarnedBadge = \"notification.action.earned_badge\"\n)\n\ntype NotificationChannelKey string\ntype NotificationSource string\n\nconst (\n\tInboxSource                          NotificationSource = \"inbox\"\n\tAllNewQuestionSource                 NotificationSource = \"all_new_question\"\n\tAllNewQuestionForFollowingTagsSource NotificationSource = \"all_new_question_for_following_tags\"\n)\n\nconst (\n\tEmailChannel NotificationChannelKey = \"email\"\n)\n\nconst (\n\tNotificationTypeInbox            = \"inbox\"\n\tNotificationTypeAchievement      = \"achievement\"\n\tNotificationTypeBadgeAchievement = \"badge\"\n)\n\nvar (\n\tNotificationMsgTypeMapping = map[string]int{\n\t\tNotificationUpdateQuestion:         1,\n\t\tNotificationAnswerTheQuestion:      1,\n\t\tNotificationUpVotedTheQuestion:     2,\n\t\tNotificationDownVotedTheQuestion:   2,\n\t\tNotificationUpdateAnswer:           1,\n\t\tNotificationAcceptAnswer:           1,\n\t\tNotificationUpVotedTheAnswer:       2,\n\t\tNotificationDownVotedTheAnswer:     2,\n\t\tNotificationCommentQuestion:        1,\n\t\tNotificationCommentAnswer:          1,\n\t\tNotificationUpVotedTheComment:      2,\n\t\tNotificationReplyToYou:             1,\n\t\tNotificationMentionYou:             1,\n\t\tNotificationYourQuestionIsClosed:   1,\n\t\tNotificationYourQuestionWasDeleted: 1,\n\t\tNotificationYourAnswerWasDeleted:   1,\n\t\tNotificationYourCommentWasDeleted:  1,\n\t\tNotificationInvitedYouToAnswer:     3,\n\t}\n)\n"
  },
  {
    "path": "internal/base/constant/object_type.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tQuestionObjectType   = \"question\"\n\tAnswerObjectType     = \"answer\"\n\tTagObjectType        = \"tag\"\n\tUserObjectType       = \"user\"\n\tCollectionObjectType = \"collection\"\n\tCommentObjectType    = \"comment\"\n\tReportObjectType     = \"report\"\n\tBadgeObjectType      = \"badge\"\n\tBadgeAwardObjectType = \"badge_award\"\n)\n\nvar (\n\tObjectTypeStrMapping = map[string]int{\n\t\tQuestionObjectType:   1,\n\t\tAnswerObjectType:     2,\n\t\tTagObjectType:        3,\n\t\tUserObjectType:       4,\n\t\tCollectionObjectType: 6,\n\t\tCommentObjectType:    7,\n\t\tReportObjectType:     8,\n\t\tBadgeObjectType:      9,\n\t\tBadgeAwardObjectType: 10,\n\t}\n\n\tObjectTypeNumberMapping = map[int]string{\n\t\t1:  QuestionObjectType,\n\t\t2:  AnswerObjectType,\n\t\t3:  TagObjectType,\n\t\t4:  UserObjectType,\n\t\t6:  CollectionObjectType,\n\t\t7:  CommentObjectType,\n\t\t8:  ReportObjectType,\n\t\t9:  BadgeObjectType,\n\t\t10: BadgeAwardObjectType,\n\t}\n)\n"
  },
  {
    "path": "internal/base/constant/plugin_config_key.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tPluginStatus = \"plugin.status\"\n)\n"
  },
  {
    "path": "internal/base/constant/privilege.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nimport \"github.com/apache/answer/internal/base/reason\"\n\ntype Privilege struct {\n\tKey   string `json:\"key\"`\n\tLabel string `json:\"label\"`\n\tValue int    `validate:\"gte=1\" json:\"value\"`\n}\n\nconst (\n\tRankQuestionAddKey               = \"rank.question.add\"\n\tRankQuestionEditKey              = \"rank.question.edit\"\n\tRankQuestionDeleteKey            = \"rank.question.delete\"\n\tRankQuestionVoteUpKey            = \"rank.question.vote_up\"\n\tRankQuestionVoteDownKey          = \"rank.question.vote_down\"\n\tRankAnswerAddKey                 = \"rank.answer.add\"\n\tRankAnswerEditKey                = \"rank.answer.edit\"\n\tRankAnswerDeleteKey              = \"rank.answer.delete\"\n\tRankAnswerAcceptKey              = \"rank.answer.accept\"\n\tRankAnswerVoteUpKey              = \"rank.answer.vote_up\"\n\tRankAnswerVoteDownKey            = \"rank.answer.vote_down\"\n\tRankInviteSomeoneToAnswerKey     = \"rank.answer.invite_someone_to_answer\"\n\tRankCommentAddKey                = \"rank.comment.add\"\n\tRankCommentEditKey               = \"rank.comment.edit\"\n\tRankCommentDeleteKey             = \"rank.comment.delete\"\n\tRankReportAddKey                 = \"rank.report.add\"\n\tRankTagAddKey                    = \"rank.tag.add\"\n\tRankTagEditKey                   = \"rank.tag.edit\"\n\tRankTagDeleteKey                 = \"rank.tag.delete\"\n\tRankTagSynonymKey                = \"rank.tag.synonym\"\n\tRankLinkUrlLimitKey              = \"rank.link.url_limit\"\n\tRankVoteDetailKey                = \"rank.vote.detail\"\n\tRankCommentVoteUpKey             = \"rank.comment.vote_up\"\n\tRankCommentVoteDownKey           = \"rank.comment.vote_down\"\n\tRankQuestionEditWithoutReviewKey = \"rank.question.edit_without_review\"\n\tRankAnswerEditWithoutReviewKey   = \"rank.answer.edit_without_review\"\n\tRankTagEditWithoutReviewKey      = \"rank.tag.edit_without_review\"\n\tRankAnswerAuditKey               = \"rank.answer.audit\"\n\tRankQuestionAuditKey             = \"rank.question.audit\"\n\tRankTagAuditKey                  = \"rank.tag.audit\"\n\tRankQuestionCloseKey             = \"rank.question.close\"\n\tRankQuestionReopenKey            = \"rank.question.reopen\"\n\tRankTagUseReservedTagKey         = \"rank.tag.use_reserved_tag\"\n)\n\nvar (\n\tRankAllPrivileges = []*Privilege{\n\t\t{Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey},\n\t\t{Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey},\n\t\t{Label: reason.RankCommentAddLabel, Key: RankCommentAddKey},\n\t\t{Label: reason.RankReportAddLabel, Key: RankReportAddKey},\n\t\t{Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},\n\t\t{Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},\n\t\t{Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},\n\t\t{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},\n\t\t{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},\n\t\t{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},\n\t\t{Label: reason.RankInviteSomeoneToAnswerLabel, Key: RankInviteSomeoneToAnswerKey},\n\t\t{Label: reason.RankTagAddLabel, Key: RankTagAddKey},\n\t\t{Label: reason.RankTagEditLabel, Key: RankTagEditKey},\n\t\t{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},\n\t\t{Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey},\n\t\t{Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},\n\t\t{Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},\n\t\t{Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey},\n\t\t{Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey},\n\t\t{Label: reason.RankTagAuditLabel, Key: RankTagAuditKey},\n\t\t{Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},\n\t\t{Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey},\n\t}\n)\n"
  },
  {
    "path": "internal/base/constant/question.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tDeletedQuestionTitleTrKey = \"question.deleted_title\"\n\tQuestionsTitleTrKey       = \"question.questions_title\"\n\tTagsListTitleTrKey        = \"tag.tags_title\"\n\tTagHasNoDescription       = \"tag.no_description\"\n)\n"
  },
  {
    "path": "internal/base/constant/reason.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tReasonSpam              = \"reason.spam\"\n\tReasonRudeOrAbusive     = \"reason.rude_or_abusive\"\n\tReasonSomething         = \"reason.something\"\n\tReasonADuplicate        = \"reason.a_duplicate\"\n\tReasonNotAAnswer        = \"reason.not_a_answer\"\n\tReasonNoLongerNeeded    = \"reason.no_longer_needed\"\n\tReasonCommunitySpecific = \"reason.community_specific\"\n\tReasonNotClarity        = \"reason.not_clarity\"\n\tReasonNormal            = \"reason.normal\"\n\tReasonNormalUser        = \"reason.normal.user\"\n\tReasonClosed            = \"reason.closed\"\n\tReasonDeleted           = \"reason.deleted\"\n\tReasonDeletedUser       = \"reason.deleted.user\"\n\tReasonSuspended         = \"reason.suspended\"\n\tReasonInactive          = \"reason.inactive\"\n\tReasonLooksOk           = \"reason.looks_ok\"\n\tReasonNeedsEdit         = \"reason.needs_edit\"\n\tReasonNeedsClose        = \"reason.needs_close\"\n\tReasonNeedsDelete       = \"reason.needs_delete\"\n)\n"
  },
  {
    "path": "internal/base/constant/revision.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\ntype ReviewingType string\n\nconst (\n\tQueuedPost        ReviewingType = \"queued_post\"\n\tQueuedUser        ReviewingType = \"queued_user\"\n\tFlaggedPost       ReviewingType = \"flagged_post\"\n\tFlaggedUser       ReviewingType = \"flagged_user\"\n\tSuggestedPostEdit ReviewingType = \"suggested_post_edit\"\n)\n\nconst (\n\tReportOperationEditPost     = \"edit_post\"\n\tReportOperationClosePost    = \"close_post\"\n\tReportOperationDeletePost   = \"delete_post\"\n\tReportOperationUnlistPost   = \"unlist_post\"\n\tReportOperationIgnoreReport = \"ignore_report\"\n)\n\nconst (\n\tReviewQueuedPostLabel        = \"review.queued_post\"\n\tReviewFlaggedPostLabel       = \"review.flagged_post\"\n\tReviewSuggestedPostEditLabel = \"review.suggested_post_edit\"\n)\n"
  },
  {
    "path": "internal/base/constant/site_info.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tDefaultGravatarBaseURL = \"https://www.gravatar.com/avatar/\"\n\tDefaultAvatar          = \"system\"\n\tAvatarTypeDefault      = \"default\"\n\tAvatarTypeGravatar     = \"gravatar\"\n\tAvatarTypeCustom       = \"custom\"\n)\n\nconst (\n\t// PermalinkQuestionIDAndTitle /questions/10010000000000001/post-title\n\tPermalinkQuestionIDAndTitle = iota + 1\n\t// PermalinkQuestionID /questions/10010000000000001\n\tPermalinkQuestionID\n\t// PermalinkQuestionIDAndTitleByShortID /questions/11/post-title\n\tPermalinkQuestionIDAndTitleByShortID\n\t// PermalinkQuestionIDByShortID /questions/11\n\tPermalinkQuestionIDByShortID\n)\n\nconst (\n\tColorSchemeDefault = \"default\"\n\tColorSchemeLight   = \"light\"\n\tColorSchemeDark    = \"dark\"\n\tColorSchemeSystem  = \"system\"\n\n\tThemeLayoutFullWidth  = \"Full-width\"\n\tThemeLayoutFixedWidth = \"Fixed-width\"\n)\n\nconst (\n\tEmailConfigKey = \"email.config\"\n)\n\nconst (\n\tDefaultMaxImageMegapixel = 40 * 1000 * 1000\n\tDefaultMaxImageSize      = 4 * 1024 * 1024\n\tDefaultMaxAttachmentSize = 8 * 1024 * 1024\n)\n"
  },
  {
    "path": "internal/base/constant/site_type.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\t// SiteTypeLegal\\SiteTypeLegal\\SiteTypeWrite The following items will no longer be used.\n\tSiteTypeLegal     = \"legal\"\n\tSiteTypeInterface = \"interface\"\n\tSiteTypeWrite     = \"write\"\n\n\tSiteTypeGeneral       = \"general\"\n\tSiteTypeBranding      = \"branding\"\n\tSiteTypeSeo           = \"seo\"\n\tSiteTypeLogin         = \"login\"\n\tSiteTypeCustomCssHTML = \"css-html\"\n\tSiteTypeTheme         = \"theme\"\n\tSiteTypePrivileges    = \"privileges\"\n\tSiteTypeUsers         = \"users\"\n\n\tSiteTypeAdvanced  = \"advanced\"\n\tSiteTypeQuestions = \"questions\"\n\tSiteTypeTags      = \"tags\"\n\n\tSiteTypeUsersSettings     = \"users_settings\"\n\tSiteTypeInterfaceSettings = \"interface_settings\"\n\n\tSiteTypePolicies      = \"policies\"\n\tSiteTypeSecurity      = \"security\"\n\tSiteTypeAI            = \"ai\"\n\tSiteTypeFeatureToggle = \"feature-toggle\"\n\tSiteTypeMCP           = \"mcp\"\n)\n"
  },
  {
    "path": "internal/base/constant/upload.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tAvatarSubPath      = \"avatar\"\n\tAvatarThumbSubPath = \"avatar_thumb\"\n\tPostSubPath        = \"post\"\n\tBrandingSubPath    = \"branding\"\n\tFilesPostSubPath   = \"files/post\"\n\tDeletedSubPath     = \"deleted\"\n)\n"
  },
  {
    "path": "internal/base/constant/user.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage constant\n\nconst (\n\tUserNormal    = \"normal\"\n\tUserSuspended = \"suspended\"\n\tUserDeleted   = \"deleted\"\n\tUserInactive  = \"inactive\"\n)\nconst (\n\tEmailStatusAvailable    = 1\n\tEmailStatusToBeVerified = 2\n)\n\nconst (\n\tDeletePermanentlyUsers     = \"users\"\n\tDeletePermanentlyQuestions = \"questions\"\n\tDeletePermanentlyAnswers   = \"answers\"\n)\n\nfunc ConvertUserStatus(status, mailStatus int) string {\n\tswitch status {\n\tcase 1:\n\t\tif mailStatus == EmailStatusToBeVerified {\n\t\t\treturn UserInactive\n\t\t}\n\t\treturn UserNormal\n\tcase 9:\n\t\treturn UserSuspended\n\tcase 10:\n\t\treturn UserDeleted\n\t}\n\treturn UserNormal\n}\n"
  },
  {
    "path": "internal/base/cron/cron.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cron\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/file_record\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/user_admin\"\n\t\"github.com/robfig/cron/v3\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// ScheduledTaskManager scheduled task manager\ntype ScheduledTaskManager struct {\n\tsiteInfoService   siteinfo_common.SiteInfoCommonService\n\tquestionService   *content.QuestionService\n\tfileRecordService *file_record.FileRecordService\n\tuserAdminService  *user_admin.UserAdminService\n\tserviceConfig     *service_config.ServiceConfig\n}\n\n// NewScheduledTaskManager new scheduled task manager\nfunc NewScheduledTaskManager(\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tquestionService *content.QuestionService,\n\tfileRecordService *file_record.FileRecordService,\n\tuserAdminService *user_admin.UserAdminService,\n\tserviceConfig *service_config.ServiceConfig,\n) *ScheduledTaskManager {\n\tmanager := &ScheduledTaskManager{\n\t\tsiteInfoService:   siteInfoService,\n\t\tquestionService:   questionService,\n\t\tfileRecordService: fileRecordService,\n\t\tuserAdminService:  userAdminService,\n\t\tserviceConfig:     serviceConfig,\n\t}\n\treturn manager\n}\n\nfunc (s *ScheduledTaskManager) Run() {\n\tlog.Infof(\"cron job manager start\")\n\n\ts.questionService.SitemapCron(context.Background())\n\tc := cron.New()\n\t_, err := c.AddFunc(\"0 */1 * * *\", func() {\n\t\tctx := context.Background()\n\t\tlog.Infof(\"sitemap cron execution\")\n\t\ts.questionService.SitemapCron(ctx)\n\t})\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\t_, err = c.AddFunc(\"0 */1 * * *\", func() {\n\t\tctx := context.Background()\n\t\tlog.Infof(\"refresh hottest cron execution\")\n\t\ts.questionService.RefreshHottestCron(ctx)\n\t})\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\t// Check for expired user suspensions every 10 minutes\n\t_, err = c.AddFunc(\"*/10 * * * *\", func() {\n\t\tctx := context.Background()\n\t\tlog.Infof(\"checking expired user suspensions\")\n\t\terr := s.userAdminService.CheckAndUnsuspendExpiredUsers(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to check expired user suspensions: %v\", err)\n\t\t}\n\t})\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tif s.serviceConfig.CleanUpUploads {\n\t\tlog.Infof(\"clean up uploads cron enabled\")\n\n\t\tconf := s.serviceConfig\n\t\t_, err = c.AddFunc(fmt.Sprintf(\"0 */%d * * *\", conf.CleanOrphanUploadsPeriodHours), func() {\n\t\t\tlog.Infof(\"clean orphan upload files cron execution\")\n\t\t\ts.fileRecordService.CleanOrphanUploadFiles(context.Background())\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\n\t\t_, err = c.AddFunc(fmt.Sprintf(\"0 0 */%d * *\", conf.PurgeDeletedFilesPeriodDays), func() {\n\t\t\tlog.Infof(\"purge deleted files cron execution\")\n\t\t\ts.fileRecordService.PurgeDeletedFiles(context.Background())\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\tc.Start()\n}\n"
  },
  {
    "path": "internal/base/cron/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cron\n\nimport (\n\t\"github.com/google/wire\"\n)\n\n// ProviderSetService is providers.\nvar ProviderSetService = wire.NewSet(\n\tNewScheduledTaskManager,\n)\n"
  },
  {
    "path": "internal/base/data/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage data\n\n// Database database config\ntype Database struct {\n\tDriver          string `json:\"driver\" mapstructure:\"driver\" yaml:\"driver\"`\n\tConnection      string `json:\"connection\" mapstructure:\"connection\" yaml:\"connection\"`\n\tConnMaxLifeTime int    `json:\"conn_max_life_time\" mapstructure:\"conn_max_life_time\" yaml:\"conn_max_life_time,omitempty\"`\n\tMaxOpenConn     int    `json:\"max_open_conn\" mapstructure:\"max_open_conn\" yaml:\"max_open_conn,omitempty\"`\n\tMaxIdleConn     int    `json:\"max_idle_conn\" mapstructure:\"max_idle_conn\" yaml:\"max_idle_conn,omitempty\"`\n}\n\n// CacheConf cache\ntype CacheConf struct {\n\tFilePath string `json:\"file_path\" mapstructure:\"file_path\" yaml:\"file_path\"`\n}\n"
  },
  {
    "path": "internal/base/data/data.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage data\n\nimport (\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/plugin\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n\t\"github.com/segmentfault/pacman/cache\"\n\t\"github.com/segmentfault/pacman/contrib/cache/memory\"\n\t\"github.com/segmentfault/pacman/log\"\n\t_ \"modernc.org/sqlite\"\n\t\"xorm.io/xorm\"\n\tormlog \"xorm.io/xorm/log\"\n\t\"xorm.io/xorm/names\"\n\t\"xorm.io/xorm/schemas\"\n)\n\n// Data data\ntype Data struct {\n\tDB    *xorm.Engine\n\tCache cache.Cache\n}\n\n// NewData new data instance\nfunc NewData(db *xorm.Engine, cache cache.Cache) (*Data, func(), error) {\n\tcleanup := func() {\n\t\tlog.Info(\"closing the data resources\")\n\t\t_ = db.Close()\n\t}\n\treturn &Data{DB: db, Cache: cache}, cleanup, nil\n}\n\n// NewDB new database instance\nfunc NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {\n\tif dataConf.Driver == \"\" {\n\t\tdataConf.Driver = string(schemas.MYSQL)\n\t}\n\tif dataConf.Driver == string(schemas.SQLITE) {\n\t\tdataConf.Driver = \"sqlite\"\n\t\tdbFileDir := filepath.Dir(dataConf.Connection)\n\t\tlog.Debugf(\"try to create database directory %s\", dbFileDir)\n\t\tif err := dir.CreateDirIfNotExist(dbFileDir); err != nil {\n\t\t\tlog.Errorf(\"create database dir failed: %s\", err)\n\t\t}\n\t\tdataConf.MaxOpenConn = 1\n\t}\n\tengine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif debug {\n\t\tengine.ShowSQL(true)\n\t} else {\n\t\tengine.SetLogLevel(ormlog.LOG_ERR)\n\t}\n\n\tif err = engine.Ping(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif dataConf.MaxIdleConn > 0 {\n\t\tengine.SetMaxIdleConns(dataConf.MaxIdleConn)\n\t}\n\tif dataConf.MaxOpenConn > 0 {\n\t\tengine.SetMaxOpenConns(dataConf.MaxOpenConn)\n\t}\n\tif dataConf.ConnMaxLifeTime > 0 {\n\t\tengine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second)\n\t}\n\tengine.SetColumnMapper(names.GonicMapper{})\n\treturn engine, nil\n}\n\n// NewCache new cache instance\nfunc NewCache(c *CacheConf) (cache.Cache, func(), error) {\n\tvar pluginCache plugin.Cache\n\t_ = plugin.CallCache(func(fn plugin.Cache) error {\n\t\tpluginCache = fn\n\t\treturn nil\n\t})\n\tif pluginCache != nil {\n\t\treturn pluginCache, func() {}, nil\n\t}\n\n\t// TODO What cache type should be initialized according to the configuration file\n\tmemCache := memory.NewCache()\n\n\tif len(c.FilePath) > 0 {\n\t\tcacheFileDir := filepath.Dir(c.FilePath)\n\t\tlog.Debugf(\"try to create cache directory %s\", cacheFileDir)\n\t\terr := dir.CreateDirIfNotExist(cacheFileDir)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"create cache dir failed: %s\", err)\n\t\t}\n\t\tlog.Infof(\"try to load cache file from %s\", c.FilePath)\n\t\tif err := memory.Load(memCache, c.FilePath); err != nil {\n\t\t\tlog.Warn(err)\n\t\t}\n\t\tgo func() {\n\t\t\tticker := time.Tick(time.Minute)\n\t\t\tfor range ticker {\n\t\t\t\tif err := memory.Save(memCache, c.FilePath); err != nil {\n\t\t\t\t\tlog.Warn(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tcleanup := func() {\n\t\tlog.Infof(\"try to save cache file to %s\", c.FilePath)\n\t\tif err := memory.Save(memCache, c.FilePath); err != nil {\n\t\t\tlog.Warn(err)\n\t\t}\n\t}\n\treturn memCache, cleanup, nil\n}\n"
  },
  {
    "path": "internal/base/handler/handler.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage handler\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/gin-gonic/gin\"\n\tmyErrors \"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// HandleResponse Handle response body\nfunc HandleResponse(ctx *gin.Context, err error, data any) {\n\tlang := GetLangByCtx(ctx)\n\t// no error\n\tif err == nil {\n\t\tctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, reason.Success, data).TrMsg(lang))\n\t\treturn\n\t}\n\n\tvar myErr *myErrors.Error\n\t// unknown error\n\tif !errors.As(err, &myErr) {\n\t\tlog.Error(err, \"\\n\", myErrors.LogStack(2, 5))\n\t\tctx.JSON(http.StatusInternalServerError, NewRespBody(\n\t\t\thttp.StatusInternalServerError, reason.UnknownError).TrMsg(lang))\n\t\treturn\n\t}\n\n\t// log internal server error\n\tif myErrors.IsInternalServer(myErr) {\n\t\tlog.Error(myErr)\n\t}\n\n\trespBody := NewRespBodyFromError(myErr).TrMsg(lang)\n\tif data != nil {\n\t\trespBody.Data = data\n\t}\n\tctx.JSON(myErr.Code, respBody)\n}\n\n// BindAndCheck bind request and check\nfunc BindAndCheck(ctx *gin.Context, data any) bool {\n\tlang := GetLangByCtx(ctx)\n\tif err := ctx.ShouldBind(data); err != nil {\n\t\tlog.Errorf(\"http_handle BindAndCheck fail, %s\", err.Error())\n\t\tHandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)\n\t\treturn true\n\t}\n\n\terrField, err := validator.GetValidatorByLang(lang).Check(data)\n\tif err != nil {\n\t\tHandleResponse(ctx, err, errField)\n\t\treturn true\n\t}\n\treturn false\n}\n\n// BindAndCheckReturnErr bind request and check\nfunc BindAndCheckReturnErr(ctx *gin.Context, data any) (errFields []*validator.FormErrorField) {\n\tlang := GetLangByCtx(ctx)\n\tif err := ctx.ShouldBind(data); err != nil {\n\t\tlog.Errorf(\"http_handle BindAndCheck fail, %s\", err.Error())\n\t\tHandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)\n\t\tctx.Abort()\n\t\treturn nil\n\t}\n\n\terrFields, _ = validator.GetValidatorByLang(lang).Check(data)\n\treturn errFields\n}\n"
  },
  {
    "path": "internal/base/handler/lang.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage handler\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\n// GetLangByCtx get language from header\nfunc GetLangByCtx(ctx context.Context) i18n.Language {\n\tif ginCtx, ok := ctx.(*gin.Context); ok {\n\t\tacceptLanguage, ok := ginCtx.Get(constant.AcceptLanguageFlag)\n\t\tif ok {\n\t\t\tif acceptLanguage, ok := acceptLanguage.(i18n.Language); ok {\n\t\t\t\treturn acceptLanguage\n\t\t\t}\n\t\t\treturn i18n.DefaultLanguage\n\t\t}\n\t}\n\n\tacceptLanguage, ok := ctx.Value(constant.AcceptLanguageContextKey).(i18n.Language)\n\tif ok {\n\t\treturn acceptLanguage\n\t}\n\treturn i18n.DefaultLanguage\n}\n"
  },
  {
    "path": "internal/base/handler/response.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage handler\n\nimport (\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\n// RespBody response body.\ntype RespBody struct {\n\t// http code\n\tCode int `json:\"code\"`\n\t// reason key\n\tReason string `json:\"reason\"`\n\t// response message\n\tMessage string `json:\"msg\"`\n\t// response data\n\tData any `json:\"data\"`\n}\n\n// TrMsg translate the reason cause as a message\nfunc (r *RespBody) TrMsg(lang i18n.Language) *RespBody {\n\tif len(r.Message) == 0 {\n\t\tr.Message = translator.Tr(lang, r.Reason)\n\t}\n\treturn r\n}\n\n// NewRespBody new response body\nfunc NewRespBody(code int, reason string) *RespBody {\n\treturn &RespBody{\n\t\tCode:   code,\n\t\tReason: reason,\n\t}\n}\n\n// NewRespBodyFromError new response body from error\nfunc NewRespBodyFromError(e *errors.Error) *RespBody {\n\treturn &RespBody{\n\t\tCode:    e.Code,\n\t\tReason:  e.Reason,\n\t\tMessage: e.Message,\n\t}\n}\n\n// NewRespBodyData new response body with data\nfunc NewRespBodyData(code int, reason string, data any) *RespBody {\n\treturn &RespBody{\n\t\tCode:   code,\n\t\tReason: reason,\n\t\tData:   data,\n\t}\n}\n"
  },
  {
    "path": "internal/base/handler/short_id.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage handler\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n)\n\n// GetEnableShortID get language from header\nfunc GetEnableShortID(ctx context.Context) bool {\n\tflag, ok := ctx.Value(constant.ShortIDContextKey).(bool)\n\tif ok {\n\t\treturn flag\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/base/middleware/accept_language.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"golang.org/x/text/language\"\n)\n\n// ExtractAndSetAcceptLanguage extract accept language from header and set to context\nfunc ExtractAndSetAcceptLanguage(ctx *gin.Context) {\n\t// The language of our front-end configuration, like en_US\n\tacceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag)\n\ttag, _, err := language.ParseAcceptLanguage(acceptLanguage)\n\tif err != nil || len(tag) == 0 {\n\t\tctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish)\n\t\treturn\n\t}\n\n\tacceptLang := strings.ReplaceAll(tag[0].String(), \"-\", \"_\")\n\n\tfor _, option := range translator.LanguageOptions {\n\t\tif option.Value == acceptLang {\n\t\t\tctx.Set(constant.AcceptLanguageFlag, i18n.Language(acceptLang))\n\t\t\treturn\n\t\t}\n\t}\n\n\t// default language\n\tctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish)\n}\n"
  },
  {
    "path": "internal/base/middleware/api_key_auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// AuthAPIKey middleware to authenticate API key\nfunc (am *AuthUserMiddleware) AuthAPIKey() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\ttoken := ExtractToken(ctx)\n\t\tif len(token) == 0 {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tpass, err := am.authService.AuthAPIKey(ctx, ctx.Request.Method == \"GET\", token)\n\t\tif err != nil {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif !pass {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tctx.Next()\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/ui\"\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nvar ctxUUIDKey = \"ctxUuidKey\"\n\n// AuthUserMiddleware auth user middleware\ntype AuthUserMiddleware struct {\n\tauthService           *auth.AuthService\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n}\n\n// NewAuthUserMiddleware new auth user middleware\nfunc NewAuthUserMiddleware(\n\tauthService *auth.AuthService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {\n\treturn &AuthUserMiddleware{\n\t\tauthService:           authService,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t}\n}\n\n// Auth get token and auth user, set user info to context if user is already login\nfunc (am *AuthUserMiddleware) Auth() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\ttoken := ExtractToken(ctx)\n\t\tif len(token) == 0 {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\t\tuserInfo, err := am.authService.GetUserCacheInfo(ctx, token)\n\t\tif err != nil {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\t\tif userInfo != nil {\n\t\t\tctx.Set(ctxUUIDKey, userInfo)\n\t\t}\n\t\tctx.Next()\n\t}\n}\n\n// EjectUserBySiteInfo if admin config the site can access by nologin user, eject user.\nfunc (am *AuthUserMiddleware) EjectUserBySiteInfo() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\tmustLogin := false\n\t\tsiteInfo, _ := am.siteInfoCommonService.GetSiteSecurity(ctx)\n\t\tif siteInfo != nil {\n\t\t\tmustLogin = siteInfo.LoginRequired\n\t\t}\n\t\tif !mustLogin {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\n\t\t// If site in private mode, user must login.\n\t\tuserInfo := GetUserInfoFromContext(ctx)\n\t\tif userInfo == nil {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\t// If user is not active, eject user.\n\t\tif userInfo.EmailStatus != entity.EmailStatusAvailable {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified),\n\t\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive})\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tctx.Next()\n\t}\n}\n\n// MustAuthWithoutAccountAvailable auth user info, any login user can access though user is not active.\nfunc (am *AuthUserMiddleware) MustAuthWithoutAccountAvailable() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\ttoken := ExtractToken(ctx)\n\t\tif len(token) == 0 {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tuserInfo, err := am.authService.GetUserCacheInfo(ctx, token)\n\t\tif err != nil || userInfo == nil {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif userInfo.UserStatus == entity.UserStatusDeleted {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tctx.Set(ctxUUIDKey, userInfo)\n\t\tctx.Next()\n\t}\n}\n\n// MustAuthAndAccountAvailable auth user info and check user status, only allow active user access.\nfunc (am *AuthUserMiddleware) MustAuthAndAccountAvailable() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\ttoken := ExtractToken(ctx)\n\t\tif len(token) == 0 {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tuserInfo, err := am.authService.GetUserCacheInfo(ctx, token)\n\t\tif err != nil || userInfo == nil {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif userInfo.EmailStatus != entity.EmailStatusAvailable {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified),\n\t\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive})\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif userInfo.UserStatus == entity.UserStatusSuspended {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),\n\t\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif userInfo.UserStatus == entity.UserStatusDeleted {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tctx.Set(ctxUUIDKey, userInfo)\n\t\tctx.Next()\n\t}\n}\n\nfunc (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\ttoken := ExtractToken(ctx)\n\t\tif len(token) == 0 {\n\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tuserInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token)\n\t\tif err != nil || userInfo == nil {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.UnauthorizedError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif userInfo != nil {\n\t\t\tif userInfo.UserStatus == entity.UserStatusDeleted {\n\t\t\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\t\t\tctx.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tctx.Set(ctxUUIDKey, userInfo)\n\t\t}\n\t\tctx.Next()\n\t}\n}\n\nfunc (am *AuthUserMiddleware) CheckPrivateMode() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\tresp, err := am.siteInfoCommonService.GetSiteSecurity(ctx)\n\t\tif err != nil {\n\t\t\tShowIndexPage(ctx)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif resp.LoginRequired {\n\t\t\tShowIndexPage(ctx)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tctx.Next()\n\t}\n}\nfunc ShowIndexPage(ctx *gin.Context) {\n\tctx.Header(\"content-type\", \"text/html;charset=utf-8\")\n\tctx.Header(\"X-Frame-Options\", \"DENY\")\n\tfile, err := ui.Build.ReadFile(\"build/index.html\")\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tctx.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\tctx.String(http.StatusOK, string(file))\n}\n\n// GetLoginUserIDFromContext get user id from context\nfunc GetLoginUserIDFromContext(ctx *gin.Context) (userID string) {\n\tuserInfo := GetUserInfoFromContext(ctx)\n\tif userInfo == nil {\n\t\treturn \"\"\n\t}\n\treturn userInfo.UserID\n}\n\n// GetIsAdminFromContext get user is admin from context\nfunc GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) {\n\tuserInfo := GetUserInfoFromContext(ctx)\n\tif userInfo == nil {\n\t\treturn false\n\t}\n\treturn userInfo.RoleID == role.RoleAdminID\n}\n\n// GetUserInfoFromContext get user info from context\nfunc GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) {\n\tuserInfo, exist := ctx.Get(ctxUUIDKey)\n\tif !exist {\n\t\treturn nil\n\t}\n\tu, ok := userInfo.(*entity.UserCacheInfo)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn u\n}\n\nfunc GetUserIsAdminModerator(ctx *gin.Context) (isAdminModerator bool) {\n\tuserInfo, exist := ctx.Get(ctxUUIDKey)\n\tif !exist {\n\t\treturn false\n\t}\n\tu, ok := userInfo.(*entity.UserCacheInfo)\n\tif !ok {\n\t\treturn false\n\t}\n\tif u.RoleID == role.RoleAdminID || u.RoleID == role.RoleModeratorID {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc GetLoginUserIDInt64FromContext(ctx *gin.Context) (userID int64) {\n\tuserIDStr := GetLoginUserIDFromContext(ctx)\n\treturn converter.StringToInt64(userIDStr)\n}\n\n// ExtractToken extract token from context\nfunc ExtractToken(ctx *gin.Context) (token string) {\n\ttoken = ctx.GetHeader(\"Authorization\")\n\tif len(token) == 0 {\n\t\ttoken = ctx.Query(\"Authorization\")\n\t}\n\treturn strings.TrimPrefix(token, \"Bearer \")\n}\n"
  },
  {
    "path": "internal/base/middleware/avatar.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/uploader\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype AvatarMiddleware struct {\n\tserviceConfig   *service_config.ServiceConfig\n\tuploaderService uploader.UploaderService\n}\n\n// NewAvatarMiddleware new auth user middleware\nfunc NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig,\n\tuploaderService uploader.UploaderService,\n) *AvatarMiddleware {\n\treturn &AvatarMiddleware{\n\t\tserviceConfig:   serviceConfig,\n\t\tuploaderService: uploaderService,\n\t}\n}\n\nfunc (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\turi := ctx.Request.RequestURI\n\t\tif strings.Contains(uri, \"/uploads/avatar/\") {\n\t\t\tsize := converter.StringToInt(ctx.Query(\"s\"))\n\t\t\turiWithoutQuery, _ := url.Parse(uri)\n\t\t\tfilename := filepath.Base(uriWithoutQuery.Path)\n\t\t\tfilePath := fmt.Sprintf(\"%s/avatar/%s\", am.serviceConfig.UploadPath, filename)\n\t\t\tvar err error\n\t\t\tif size != 0 {\n\t\t\t\tfilePath, err = am.uploaderService.AvatarThumbFile(ctx, filename, size)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t\tctx.AbortWithStatus(http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tavatarFile, err := os.ReadFile(filePath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\tctx.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tctx.Header(\"content-type\", fmt.Sprintf(\"image/%s\", strings.TrimLeft(path.Ext(filePath), \".\")))\n\t\t\t_, err = ctx.Writer.Write(avatarFile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t} else {\n\t\t\turlInfo, err := url.Parse(uri)\n\t\t\tif err != nil {\n\t\t\t\tctx.Next()\n\t\t\t\treturn\n\t\t\t}\n\t\t\text := strings.TrimPrefix(filepath.Ext(urlInfo.Path), \".\")\n\t\t\tctx.Header(\"content-type\", fmt.Sprintf(\"image/%s\", ext))\n\t\t}\n\t\tctx.Next()\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/header.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc HeadersByRequestURI() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tif strings.HasPrefix(c.Request.RequestURI, \"/static/\") {\n\t\t\tc.Header(\"cache-control\", \"public, max-age=31536000\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/mcp_auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// AuthMcpEnable check mcp is enabled\nfunc (am *AuthUserMiddleware) AuthMcpEnable() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\tmcpConfig, err := am.siteInfoCommonService.GetSiteMCP(ctx)\n\t\tif err != nil {\n\t\t\thandler.HandleResponse(ctx, errors.InternalServer(reason.UnknownError), nil)\n\t\t\tctx.Abort()\n\t\t\treturn\n\t\t}\n\t\tif mcpConfig != nil && mcpConfig.Enabled {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\tctx.Abort()\n\t\tlog.Error(\"abort mcp auth middleware, get mcp config error: \", err)\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"github.com/google/wire\"\n)\n\n// ProviderSetMiddleware is providers.\nvar ProviderSetMiddleware = wire.NewSet(\n\tNewAuthUserMiddleware,\n\tNewAvatarMiddleware,\n\tNewShortIDMiddleware,\n\tNewRateLimitMiddleware,\n)\n"
  },
  {
    "path": "internal/base/middleware/rate_limit.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/repo/limit\"\n\t\"github.com/apache/answer/pkg/encryption\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype RateLimitMiddleware struct {\n\tlimitRepo *limit.LimitRepo\n}\n\n// NewRateLimitMiddleware new rate limit middleware\nfunc NewRateLimitMiddleware(limitRepo *limit.LimitRepo) *RateLimitMiddleware {\n\treturn &RateLimitMiddleware{\n\t\tlimitRepo: limitRepo,\n\t}\n}\n\n// DuplicateRequestRejection detects and rejects duplicate requests\n// It only works for the requests that post content. Such as add question, add answer, comment etc.\nfunc (rm *RateLimitMiddleware) DuplicateRequestRejection(ctx *gin.Context, req any) (reject bool, key string) {\n\tuserID := GetLoginUserIDFromContext(ctx)\n\tfullPath := ctx.FullPath()\n\treqJson, _ := json.Marshal(req)\n\tkey = encryption.MD5(fmt.Sprintf(\"%s:%s:%s\", userID, fullPath, string(reqJson)))\n\tvar err error\n\treject, err = rm.limitRepo.CheckAndRecord(ctx, key)\n\tif err != nil {\n\t\tlog.Errorf(\"check and record rate limit error: %s\", err.Error())\n\t\treturn false, key\n\t}\n\tif !reject {\n\t\treturn false, key\n\t}\n\tlog.Debugf(\"duplicate request: [%s] %s\", fullPath, string(reqJson))\n\thandler.HandleResponse(ctx, errors.BadRequest(reason.DuplicateRequestError), nil)\n\treturn true, key\n}\n\n// DuplicateRequestClear clear duplicate request record\nfunc (rm *RateLimitMiddleware) DuplicateRequestClear(ctx *gin.Context, key string) {\n\terr := rm.limitRepo.ClearRecord(ctx, key)\n\tif err != nil {\n\t\tlog.Errorf(\"clear rate limit error: %s\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/short_id.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype ShortIDMiddleware struct {\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n}\n\nfunc NewShortIDMiddleware(siteInfoService siteinfo_common.SiteInfoCommonService) *ShortIDMiddleware {\n\treturn &ShortIDMiddleware{\n\t\tsiteInfoService: siteInfoService,\n\t}\n}\n\nfunc (sm *ShortIDMiddleware) SetShortIDFlag() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\tsiteSeo, err := sm.siteInfoService.GetSiteSeo(ctx)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tctx.Set(constant.ShortIDFlag, siteSeo.IsShortLink())\n\t}\n}\n"
  },
  {
    "path": "internal/base/middleware/user_center_plugin_auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// BanAPIForUserCenter ban api for user center\nfunc BanAPIForUserCenter(ctx *gin.Context) {\n\tuc, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn\n\t}\n\tif !uc.Description().EnabledOriginalUserSystem {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\tctx.Abort()\n\t\treturn\n\t}\n\tctx.Next()\n}\n"
  },
  {
    "path": "internal/base/middleware/visit_img_auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// VisitAuth when user visit the site image, check visit token. This only for private mode.\nfunc (am *AuthUserMiddleware) VisitAuth() gin.HandlerFunc {\n\treturn func(ctx *gin.Context) {\n\t\tif len(os.Getenv(\"SKIP_FILE_ACCESS_VERIFY\")) > 0 {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\t\t// If visit brand image, no need to check visit token. Because the brand image is public.\n\t\tif strings.HasPrefix(ctx.Request.URL.Path, \"/uploads/branding/\") {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\n\t\tsiteSecurity, err := am.siteInfoCommonService.GetSiteSecurity(ctx)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tif !siteSecurity.LoginRequired {\n\t\t\tctx.Next()\n\t\t\treturn\n\t\t}\n\n\t\tvisitToken, err := ctx.Cookie(constant.UserVisitCookiesCacheKey)\n\t\tif err != nil || len(visitToken) == 0 {\n\t\t\tctx.Abort()\n\t\t\tctx.Redirect(http.StatusFound, \"/403\")\n\t\t\treturn\n\t\t}\n\n\t\tif !am.authService.CheckUserVisitToken(ctx, visitToken) {\n\t\t\tctx.Abort()\n\t\t\tctx.Redirect(http.StatusFound, \"/403\")\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/base/pager/pager.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage pager\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\n\t\"xorm.io/xorm\"\n)\n\n// Help xorm page helper\nfunc Help(page, pageSize int, rowsSlicePtr any, rowElement any, session *xorm.Session) (total int64, err error) {\n\tpage, pageSize = ValPageAndPageSize(page, pageSize)\n\n\tsliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr))\n\tif sliceValue.Kind() != reflect.Slice {\n\t\treturn 0, errors.New(\"not a slice\")\n\t}\n\n\tstartNum := (page - 1) * pageSize\n\treturn session.Limit(pageSize, startNum).FindAndCount(rowsSlicePtr, rowElement)\n}\n"
  },
  {
    "path": "internal/base/pager/pagination.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage pager\n\nimport (\n\t\"reflect\"\n)\n\n// PageModel page model\ntype PageModel struct {\n\tCount int64 `json:\"count\"`\n\tList  any   `json:\"list\"`\n}\n\n// PageCond page condition\ntype PageCond struct {\n\tPage     int\n\tPageSize int\n}\n\n// NewPageModel new page model\nfunc NewPageModel(totalRecords int64, records any) *PageModel {\n\tsliceValue := reflect.Indirect(reflect.ValueOf(records))\n\tif sliceValue.Kind() != reflect.Slice {\n\t\tpanic(\"not a slice\")\n\t}\n\n\tif totalRecords < 0 {\n\t\ttotalRecords = 0\n\t}\n\n\treturn &PageModel{\n\t\tCount: totalRecords,\n\t\tList:  records,\n\t}\n}\n\n// ValPageAndPageSize validate page pageSize\nfunc ValPageAndPageSize(page, pageSize int) (int, int) {\n\tif page <= 0 {\n\t\tpage = 1\n\t}\n\tif pageSize <= 0 {\n\t\tpageSize = 10\n\t}\n\treturn page, pageSize\n}\n\n// ValPageOutOfRange validate page out of range\nfunc ValPageOutOfRange(total int64, page, pageSize int) bool {\n\tif total <= 0 {\n\t\treturn false\n\t}\n\tif pageSize <= 0 {\n\t\treturn true\n\t}\n\ttotalPages := (total + int64(pageSize) - 1) / int64(pageSize)\n\treturn page < 1 || page > int(totalPages)\n}\n"
  },
  {
    "path": "internal/base/path/path.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage path\n\nimport (\n\t\"path/filepath\"\n\t\"sync\"\n)\n\nconst (\n\tDefaultConfigFileName                  = \"config.yaml\"\n\tDefaultCacheFileName                   = \"cache.db\"\n\tDefaultReservedUsernamesConfigFileName = \"reserved-usernames.json\"\n)\n\nvar (\n\tConfigFileDir     = \"/conf/\"\n\tUploadFilePath    = \"/uploads/\"\n\tI18nPath          = \"/i18n/\"\n\tCacheDir          = \"/cache/\"\n\tformatAllPathOnce sync.Once\n)\n\nfunc FormatAllPath(dataDirPath string) {\n\tformatAllPathOnce.Do(func() {\n\t\tConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)\n\t\tUploadFilePath = filepath.Join(dataDirPath, UploadFilePath)\n\t\tI18nPath = filepath.Join(dataDirPath, I18nPath)\n\t\tCacheDir = filepath.Join(dataDirPath, CacheDir)\n\t})\n}\n\n// GetConfigFilePath get config file path\nfunc GetConfigFilePath() string {\n\treturn filepath.Join(ConfigFileDir, DefaultConfigFileName)\n}\n"
  },
  {
    "path": "internal/base/queue/queue.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage queue\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype Service[T any] interface {\n\t// Send enqueues a message to be processed asynchronously.\n\tSend(ctx context.Context, msg T)\n\n\t// RegisterHandler sets the handler function for processing messages.\n\tRegisterHandler(handler func(ctx context.Context, msg T) error)\n\n\t// Close gracefully shuts down the queue, waiting for pending messages to be processed.\n\tClose()\n}\n\n// Queue is a generic message queue service that processes messages asynchronously.\n// It is thread-safe and supports graceful shutdown.\ntype Queue[T any] struct {\n\tname    string\n\tqueue   chan T\n\thandler func(ctx context.Context, msg T) error\n\tmu      sync.RWMutex\n\tclosed  bool\n\twg      sync.WaitGroup\n}\n\n// New creates a new queue with the given name and buffer size.\nfunc New[T any](name string, bufferSize int) *Queue[T] {\n\tq := &Queue[T]{\n\t\tname:  name,\n\t\tqueue: make(chan T, bufferSize),\n\t}\n\tq.startWorker()\n\treturn q\n}\n\n// Send enqueues a message to be processed asynchronously.\n// It will block if the queue is full.\nfunc (q *Queue[T]) Send(ctx context.Context, msg T) {\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\n\tif q.closed {\n\t\tlog.Warnf(\"[%s] queue is closed, dropping message\", q.name)\n\t\treturn\n\t}\n\n\tselect {\n\tcase q.queue <- msg:\n\t\tlog.Debugf(\"[%s] enqueued message: %+v\", q.name, msg)\n\tcase <-ctx.Done():\n\t\tlog.Warnf(\"[%s] context cancelled while sending message\", q.name)\n\t}\n}\n\n// RegisterHandler sets the handler function for processing messages.\n// This is thread-safe and can be called at any time.\nfunc (q *Queue[T]) RegisterHandler(handler func(ctx context.Context, msg T) error) {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\tq.handler = handler\n}\n\n// Close gracefully shuts down the queue, waiting for pending messages to be processed.\nfunc (q *Queue[T]) Close() {\n\tq.mu.Lock()\n\tif q.closed {\n\t\tq.mu.Unlock()\n\t\treturn\n\t}\n\tq.closed = true\n\tq.mu.Unlock()\n\n\tclose(q.queue)\n\tq.wg.Wait()\n\tlog.Infof(\"[%s] queue closed\", q.name)\n}\n\n// startWorker starts the background goroutine that processes messages.\nfunc (q *Queue[T]) startWorker() {\n\tq.wg.Add(1)\n\tgo func() {\n\t\tdefer q.wg.Done()\n\t\tfor msg := range q.queue {\n\t\t\tq.processMessage(msg)\n\t\t}\n\t}()\n}\n\n// processMessage handles a single message with proper synchronization.\nfunc (q *Queue[T]) processMessage(msg T) {\n\tq.mu.RLock()\n\thandler := q.handler\n\tq.mu.RUnlock()\n\n\tif handler == nil {\n\t\tlog.Warnf(\"[%s] no handler registered, dropping message: %+v\", q.name, msg)\n\t\treturn\n\t}\n\n\t// Use background context for async processing\n\t// TODO: Consider adding timeout or using a derived context\n\tif err := handler(context.TODO(), msg); err != nil {\n\t\tlog.Errorf(\"[%s] handler error: %v\", q.name, err)\n\t}\n}\n"
  },
  {
    "path": "internal/base/queue/queue_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype testMessage struct {\n\tID   int\n\tData string\n}\n\nfunc TestQueue_SendAndReceive(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\tdefer q.Close()\n\n\treceived := make(chan *testMessage, 1)\n\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\treceived <- msg\n\t\treturn nil\n\t})\n\n\tmsg := &testMessage{ID: 1, Data: \"hello\"}\n\tq.Send(context.Background(), msg)\n\n\tselect {\n\tcase r := <-received:\n\t\tif r.ID != msg.ID || r.Data != msg.Data {\n\t\t\tt.Errorf(\"received message mismatch: got %+v, want %+v\", r, msg)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timeout waiting for message\")\n\t}\n}\n\nfunc TestQueue_MultipleMessages(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\tdefer q.Close()\n\n\tvar count atomic.Int32\n\tvar wg sync.WaitGroup\n\tnumMessages := 100\n\twg.Add(numMessages)\n\n\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\tcount.Add(1)\n\t\twg.Done()\n\t\treturn nil\n\t})\n\n\tfor i := range numMessages {\n\t\tq.Send(context.Background(), &testMessage{ID: i})\n\t}\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\tif int(count.Load()) != numMessages {\n\t\t\tt.Errorf(\"expected %d messages, got %d\", numMessages, count.Load())\n\t\t}\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatalf(\"timeout: only received %d of %d messages\", count.Load(), numMessages)\n\t}\n}\n\nfunc TestQueue_NoHandlerDropsMessage(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\tdefer q.Close()\n\n\t// Send without handler - should not panic\n\tq.Send(context.Background(), &testMessage{ID: 1})\n\n\t// Give time for the message to be processed (dropped)\n\ttime.Sleep(100 * time.Millisecond)\n}\n\nfunc TestQueue_RegisterHandlerAfterSend(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\tdefer q.Close()\n\n\treceived := make(chan *testMessage, 1)\n\n\t// Send first\n\tq.Send(context.Background(), &testMessage{ID: 1})\n\n\t// Small delay then register handler\n\ttime.Sleep(50 * time.Millisecond)\n\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\treceived <- msg\n\t\treturn nil\n\t})\n\n\t// Send another message that should be received\n\tq.Send(context.Background(), &testMessage{ID: 2})\n\n\tselect {\n\tcase r := <-received:\n\t\tif r.ID != 2 {\n\t\t\t// First message was dropped (no handler), second should be received\n\t\t\tt.Logf(\"received message ID: %d\", r.ID)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timeout waiting for message\")\n\t}\n}\n\nfunc TestQueue_Close(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\n\tvar count atomic.Int32\n\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\tcount.Add(1)\n\t\treturn nil\n\t})\n\n\t// Send some messages\n\tfor i := range 5 {\n\t\tq.Send(context.Background(), &testMessage{ID: i})\n\t}\n\n\t// Close and wait\n\tq.Close()\n\n\t// All messages should have been processed\n\tif count.Load() != 5 {\n\t\tt.Errorf(\"expected 5 messages processed, got %d\", count.Load())\n\t}\n\n\t// Sending after close should not panic\n\tq.Send(context.Background(), &testMessage{ID: 99})\n}\n\nfunc TestQueue_ConcurrentSend(t *testing.T) {\n\tq := New[*testMessage](\"test\", 100)\n\tdefer q.Close()\n\n\tvar count atomic.Int32\n\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\tcount.Add(1)\n\t\treturn nil\n\t})\n\n\tvar wg sync.WaitGroup\n\tnumGoroutines := 10\n\tmessagesPerGoroutine := 100\n\n\tfor i := range numGoroutines {\n\t\twg.Add(1)\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := range messagesPerGoroutine {\n\t\t\t\tq.Send(context.Background(), &testMessage{ID: id*1000 + j})\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Wait for processing\n\ttime.Sleep(500 * time.Millisecond)\n\n\texpected := int32(numGoroutines * messagesPerGoroutine)\n\tif count.Load() != expected {\n\t\tt.Errorf(\"expected %d messages, got %d\", expected, count.Load())\n\t}\n}\n\nfunc TestQueue_ConcurrentRegisterHandler(t *testing.T) {\n\tq := New[*testMessage](\"test\", 10)\n\tdefer q.Close()\n\n\t// Concurrently register handlers - should not race\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}()\n\t}\n\twg.Wait()\n}\n\n// TestQueue_SendCloseRace is a regression test for the race condition between\n// Send and Close. Without proper synchronization, concurrent Send and Close\n// calls could cause a \"send on closed channel\" panic.\n// Run with: go test -race -run TestQueue_SendCloseRace\nfunc TestQueue_SendCloseRace(t *testing.T) {\n\tfor i := range 100 {\n\t\tt.Run(fmt.Sprintf(\"iteration_%d\", i), func(t *testing.T) {\n\t\t\t// Use large buffer to avoid blocking on channel send while holding RLock\n\t\t\tq := New[*testMessage](\"test-race\", 1000)\n\t\t\tq.RegisterHandler(func(ctx context.Context, msg *testMessage) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tvar wg sync.WaitGroup\n\n\t\t\t// Use cancellable context so senders can exit when Close is called\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\t\t// Start multiple senders\n\t\t\tfor j := range 10 {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(id int) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tfor k := range 100 {\n\t\t\t\t\t\tq.Send(ctx, &testMessage{ID: id*1000 + k})\n\t\t\t\t\t}\n\t\t\t\t}(j)\n\t\t\t}\n\n\t\t\t// Close while senders are still running\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(time.Microsecond * 10)\n\t\t\t\tcancel() // Cancel context to unblock any waiting senders\n\t\t\t\tq.Close()\n\t\t\t}()\n\n\t\t\twg.Wait()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/base/reason/privilege.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage reason\n\nconst (\n\tPrivilegeLevel1Desc      = \"privilege.level_1.description\"\n\tPrivilegeLevel2Desc      = \"privilege.level_2.description\"\n\tPrivilegeLevel3Desc      = \"privilege.level_3.description\"\n\tPrivilegeLevelCustomDesc = \"privilege.level_custom.description\"\n\n\tRankQuestionAddLabel               = \"privilege.rank_question_add_label\"\n\tRankAnswerAddLabel                 = \"privilege.rank_answer_add_label\"\n\tRankCommentAddLabel                = \"privilege.rank_comment_add_label\"\n\tRankReportAddLabel                 = \"privilege.rank_report_add_label\"\n\tRankCommentVoteUpLabel             = \"privilege.rank_comment_vote_up_label\"\n\tRankLinkUrlLimitLabel              = \"privilege.rank_link_url_limit_label\"\n\tRankQuestionVoteUpLabel            = \"privilege.rank_question_vote_up_label\"\n\tRankAnswerVoteUpLabel              = \"privilege.rank_answer_vote_up_label\"\n\tRankQuestionVoteDownLabel          = \"privilege.rank_question_vote_down_label\"\n\tRankAnswerVoteDownLabel            = \"privilege.rank_answer_vote_down_label\"\n\tRankInviteSomeoneToAnswerLabel     = \"privilege.rank_invite_someone_to_answer_label\"\n\tRankTagAddLabel                    = \"privilege.rank_tag_add_label\"\n\tRankTagEditLabel                   = \"privilege.rank_tag_edit_label\"\n\tRankQuestionEditLabel              = \"privilege.rank_question_edit_label\"\n\tRankAnswerEditLabel                = \"privilege.rank_answer_edit_label\"\n\tRankQuestionEditWithoutReviewLabel = \"privilege.rank_question_edit_without_review_label\"\n\tRankAnswerEditWithoutReviewLabel   = \"privilege.rank_answer_edit_without_review_label\"\n\tRankQuestionAuditLabel             = \"privilege.rank_question_audit_label\"\n\tRankAnswerAuditLabel               = \"privilege.rank_answer_audit_label\"\n\tRankTagAuditLabel                  = \"privilege.rank_tag_audit_label\"\n\tRankTagEditWithoutReviewLabel      = \"privilege.rank_tag_edit_without_review_label\"\n\tRankTagSynonymLabel                = \"privilege.rank_tag_synonym_label\"\n)\n"
  },
  {
    "path": "internal/base/reason/reason.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage reason\n\nconst (\n\t// Success .\n\tSuccess = \"base.success\"\n\t// UnknownError unknown error\n\tUnknownError = \"base.unknown\"\n\t// RequestFormatError request format error\n\tRequestFormatError = \"base.request_format_error\"\n\t// UnauthorizedError unauthorized error\n\tUnauthorizedError = \"base.unauthorized_error\"\n\t// DatabaseError database error\n\tDatabaseError = \"base.database_error\"\n\t// ForbiddenError forbidden error\n\tForbiddenError = \"base.forbidden_error\"\n\t// DuplicateRequestError duplicate request error\n\tDuplicateRequestError = \"base.duplicate_request_error\"\n)\n\nconst (\n\tEmailOrPasswordWrong             = \"error.object.email_or_password_incorrect\"\n\tCommentNotFound                  = \"error.comment.not_found\"\n\tCommentCannotEditAfterDeadline   = \"error.comment.cannot_edit_after_deadline\"\n\tQuestionNotFound                 = \"error.question.not_found\"\n\tQuestionCannotDeleted            = \"error.question.cannot_deleted\"\n\tQuestionCannotClose              = \"error.question.cannot_close\"\n\tQuestionCannotUpdate             = \"error.question.cannot_update\"\n\tQuestionAlreadyDeleted           = \"error.question.already_deleted\"\n\tQuestionUnderReview              = \"error.question.under_review\"\n\tQuestionContentCannotEmpty       = \"error.question.content_cannot_empty\"\n\tQuestionContentLessThanMinimum   = \"error.question.content_less_than_minimum\"\n\tAnswerNotFound                   = \"error.answer.not_found\"\n\tAnswerCannotDeleted              = \"error.answer.cannot_deleted\"\n\tAnswerCannotUpdate               = \"error.answer.cannot_update\"\n\tAnswerCannotAddByClosedQuestion  = \"error.answer.question_closed_cannot_add\"\n\tAnswerRestrictAnswer             = \"error.answer.restrict_answer\"\n\tAnswerContentCannotEmpty         = \"error.answer.content_cannot_empty\"\n\tCommentEditWithoutPermission     = \"error.comment.edit_without_permission\"\n\tCommentContentCannotEmpty        = \"error.comment.content_cannot_empty\"\n\tDisallowVote                     = \"error.object.disallow_vote\"\n\tDisallowFollow                   = \"error.object.disallow_follow\"\n\tDisallowVoteYourSelf             = \"error.object.disallow_vote_your_self\"\n\tCaptchaVerificationFailed        = \"error.object.captcha_verification_failed\"\n\tOldPasswordVerificationFailed    = \"error.object.old_password_verification_failed\"\n\tNewPasswordSameAsPreviousSetting = \"error.object.new_password_same_as_previous_setting\"\n\tNewObjectAlreadyDeleted          = \"error.object.already_deleted\"\n\tUserNotFound                     = \"error.user.not_found\"\n\tUsernameInvalid                  = \"error.user.username_invalid\"\n\tUsernameDuplicate                = \"error.user.username_duplicate\"\n\tUserSetAvatar                    = \"error.user.set_avatar\"\n\tEmailDuplicate                   = \"error.email.duplicate\"\n\tEmailVerifyURLExpired            = \"error.email.verify_url_expired\"\n\tEmailNeedToBeVerified            = \"error.email.need_to_be_verified\"\n\tEmailIllegalDomainError          = \"error.email.illegal_email_domain_error\"\n\tUserSuspended                    = \"error.user.suspended\"\n\tObjectNotFound                   = \"error.object.not_found\"\n\tTagNotFound                      = \"error.tag.not_found\"\n\tTagNotContainSynonym             = \"error.tag.not_contain_synonym_tags\"\n\tTagCannotUpdate                  = \"error.tag.cannot_update\"\n\tTagIsUsedCannotDelete            = \"error.tag.is_used_cannot_delete\"\n\tTagAlreadyExist                  = \"error.tag.already_exist\"\n\tTagMinCount                      = \"error.tag.minimum_count\"\n\tRankFailToMeetTheCondition       = \"error.rank.fail_to_meet_the_condition\"\n\tVoteRankFailToMeetTheCondition   = \"error.rank.vote_fail_to_meet_the_condition\"\n\tNoEnoughRankToOperate            = \"error.rank.no_enough_rank_to_operate\"\n\tThemeNotFound                    = \"error.theme.not_found\"\n\tLangNotFound                     = \"error.lang.not_found\"\n\tReportHandleFailed               = \"error.report.handle_failed\"\n\tReportNotFound                   = \"error.report.not_found\"\n\tReadConfigFailed                 = \"error.config.read_config_failed\"\n\tDatabaseConnectionFailed         = \"error.database.connection_failed\"\n\tInstallCreateTableFailed         = \"error.database.create_table_failed\"\n\tInstallConfigFailed              = \"error.install.create_config_failed\"\n\tSiteInfoConfigNotFound           = \"error.site_info.config_not_found\"\n\tUploadFileSourceUnsupported      = \"error.upload.source_unsupported\"\n\tUploadFileUnsupportedFileFormat  = \"error.upload.unsupported_file_format\"\n\tRecommendTagNotExist             = \"error.tag.recommend_tag_not_found\"\n\tRecommendTagEnter                = \"error.tag.recommend_tag_enter\"\n\tRevisionReviewUnderway           = \"error.revision.review_underway\"\n\tRevisionNoPermission             = \"error.revision.no_permission\"\n\tUserCannotUpdateYourRole         = \"error.user.cannot_update_your_role\"\n\tTagCannotSetSynonymAsItself      = \"error.tag.cannot_set_synonym_as_itself\"\n\tNotAllowedRegistration           = \"error.user.not_allowed_registration\"\n\tNotAllowedLoginViaPassword       = \"error.user.not_allowed_login_via_password\"\n\tSMTPConfigFromNameCannotBeEmail  = \"error.smtp.config_from_name_cannot_be_email\"\n\tAdminCannotUpdateTheirPassword   = \"error.admin.cannot_update_their_password\"\n\tAdminCannotEditTheirProfile      = \"error.admin.cannot_edit_their_profile\"\n\tAdminCannotModifySelfStatus      = \"error.admin.cannot_modify_self_status\"\n\tUserAccessDenied                 = \"error.user.access_denied\"\n\tUserPageAccessDenied             = \"error.user.page_access_denied\"\n\tAddBulkUsersFormatError          = \"error.user.add_bulk_users_format_error\"\n\tAddBulkUsersAmountError          = \"error.user.add_bulk_users_amount_error\"\n\tInvalidURLError                  = \"error.common.invalid_url\"\n\tMetaObjectNotFound               = \"error.meta.object_not_found\"\n\tBadgeObjectNotFound              = \"error.badge.object_not_found\"\n\tStatusInvalid                    = \"error.common.status_invalid\"\n\tUserStatusInactive               = \"error.user.status_inactive\"\n\tUserStatusSuspendedForever       = \"error.user.status_suspended_forever\"\n\tUserStatusSuspendedUntil         = \"error.user.status_suspended_until\"\n\tUserStatusDeleted                = \"error.user.status_deleted\"\n\tErrFeatureDisabled               = \"error.feature.disabled\"\n)\n\n// user external login reasons\nconst (\n\tUserExternalLoginUnbindingForbidden = \"error.user.external_login_unbinding_forbidden\"\n\tUserExternalLoginMissingUserID      = \"error.user.external_login_missing_user_id\"\n)\n"
  },
  {
    "path": "internal/base/server/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage server\n\n// HTTP http config\ntype HTTP struct {\n\tAddr string `json:\"addr\" mapstructure:\"addr\"`\n}\n\n// UI ui config\ntype UI struct {\n\tBaseURL    string `json:\"base_url\" mapstructure:\"base_url\" yaml:\"base_url\"`\n\tAPIBaseURL string `json:\"api_base_url\" mapstructure:\"api_base_url\" yaml:\"api_base_url\"`\n}\n"
  },
  {
    "path": "internal/base/server/http.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage server\n\nimport (\n\t\"html/template\"\n\t\"io/fs\"\n\t\"os\"\n\t\"strings\"\n\n\tbrotli \"github.com/anargu/gin-brotli\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/router\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/apache/answer/ui\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// NewHTTPServer new http server.\nfunc NewHTTPServer(debug bool,\n\tstaticRouter *router.StaticRouter,\n\tanswerRouter *router.AnswerAPIRouter,\n\tswaggerRouter *router.SwaggerRouter,\n\tviewRouter *router.UIRouter,\n\tauthUserMiddleware *middleware.AuthUserMiddleware,\n\tavatarMiddleware *middleware.AvatarMiddleware,\n\tshortIDMiddleware *middleware.ShortIDMiddleware,\n\ttemplateRouter *router.TemplateRouter,\n\tpluginAPIRouter *router.PluginAPIRouter,\n\tuiConf *UI,\n) *gin.Engine {\n\tif debug {\n\t\tgin.SetMode(gin.DebugMode)\n\t} else {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t}\n\tr := gin.New()\n\tr.Use(func(ctx *gin.Context) {\n\t\tif strings.Contains(ctx.Request.URL.Path, \"/chat/completions\") {\n\t\t\treturn\n\t\t}\n\t\tbrotli.Brotli(brotli.DefaultCompression)(ctx)\n\t}, middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag())\n\tr.GET(\"/healthz\", func(ctx *gin.Context) { ctx.String(200, \"OK\") })\n\n\ttemplatePath := os.Getenv(\"ANSWER_TEMPLATE_PATH\")\n\tif templatePath != \"\" {\n\t\tr.LoadHTMLGlob(templatePath)\n\t} else {\n\t\thtml, _ := fs.Sub(ui.Template, \"template\")\n\t\thtmlTemplate := template.Must(template.New(\"\").Funcs(funcMap).ParseFS(html, \"*\"))\n\t\tr.SetHTMLTemplate(htmlTemplate)\n\t}\n\tr.Use(middleware.HeadersByRequestURI())\n\tviewRouter.Register(r, uiConf.BaseURL)\n\n\trootGroup := r.Group(\"\")\n\tswaggerRouter.Register(rootGroup)\n\tstatic := r.Group(uiConf.APIBaseURL)\n\tstatic.Use(avatarMiddleware.AvatarThumb(), authUserMiddleware.VisitAuth())\n\tstaticRouter.RegisterStaticRouter(static)\n\n\t// The route must be available without logging in\n\tmustUnAuthV1 := r.Group(uiConf.APIBaseURL + \"/answer/api/v1\")\n\tanswerRouter.RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware, mustUnAuthV1)\n\n\t// register api that no need to login\n\tunAuthV1 := r.Group(uiConf.APIBaseURL + \"/answer/api/v1\")\n\tunAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo())\n\tanswerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1)\n\n\t// register api that must be authenticated but no need to check account status\n\tauthWithoutStatusV1 := r.Group(uiConf.APIBaseURL + \"/answer/api/v1\")\n\tauthWithoutStatusV1.Use(authUserMiddleware.MustAuthWithoutAccountAvailable())\n\tanswerRouter.RegisterAuthUserWithAnyStatusAnswerAPIRouter(authWithoutStatusV1)\n\n\t// register api that must be authenticated\n\tauthV1 := r.Group(uiConf.APIBaseURL + \"/answer/api/v1\")\n\tauthV1.Use(authUserMiddleware.MustAuthAndAccountAvailable())\n\tanswerRouter.RegisterAnswerAPIRouter(authV1)\n\n\tadminauthV1 := r.Group(uiConf.APIBaseURL + \"/answer/admin/api\")\n\tadminauthV1.Use(authUserMiddleware.AdminAuth())\n\tanswerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)\n\n\ttemplateRouter.RegisterTemplateRouter(rootGroup, uiConf.BaseURL)\n\n\t// plugin routes\n\tpluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)\n\tpluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)\n\tpluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)\n\n\t_ = plugin.CallAgent(func(agent plugin.Agent) error {\n\t\tagent.RegisterUnAuthRouter(mustUnAuthV1)\n\t\tagent.RegisterAuthUserRouter(authV1)\n\t\tagent.RegisterAuthAdminRouter(adminauthV1)\n\t\treturn nil\n\t})\n\n\t// mcp\n\tmcpAPIGroup := r.Group(uiConf.APIBaseURL + \"/answer/api/v1\")\n\tmcpAPIGroup.Use(authUserMiddleware.AuthMcpEnable(), authUserMiddleware.AuthAPIKey())\n\tanswerRouter.RegisterMCPRouter(mcpAPIGroup)\n\treturn r\n}\n"
  },
  {
    "path": "internal/base/server/http_funcmap.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage server\n\nimport (\n\t\"html/template\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/controller\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/day\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\nvar funcMap = template.FuncMap{\n\t\"replaceHTMLTag\": func(src string, tags ...string) string {\n\t\tp := `(?U)<(\\d+)>.+</(\\d+)>`\n\n\t\tre := regexp.MustCompile(p)\n\t\tms := re.FindAllStringSubmatch(src, -1)\n\t\tfor _, mi := range ms {\n\t\t\tif mi[1] == mi[2] {\n\t\t\t\ti, err := strconv.Atoi(mi[1])\n\t\t\t\tif err != nil || len(tags) < i {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsrc = strings.ReplaceAll(src, mi[0], tags[i-1])\n\t\t\t}\n\t\t}\n\n\t\treturn src\n\t},\n\t\"join\": func(sep string, elems ...string) string {\n\t\treturn strings.Join(elems, sep)\n\t},\n\t\"templateHTML\": func(data string) template.HTML {\n\t\treturn template.HTML(data)\n\t},\n\t\"formatLinkNofollow\": func(data string) template.HTML {\n\t\treturn template.HTML(FormatLinkNofollow(data))\n\t},\n\t\"translator\": func(la i18n.Language, data string, params ...any) string {\n\t\ttrans := translator.GlobalTrans.Tr(la, data)\n\n\t\tif len(params) > 0 && len(params)%2 == 0 {\n\t\t\tfor i := 0; i < len(params); i += 2 {\n\t\t\t\tk := converter.InterfaceToString(params[i])\n\t\t\t\tv := converter.InterfaceToString(params[i+1])\n\t\t\t\ttrans = strings.ReplaceAll(trans, \"{{ \"+k+\" }}\", v)\n\t\t\t\ttrans = strings.ReplaceAll(trans, \"{{\"+k+\"}}\", v)\n\t\t\t}\n\t\t}\n\n\t\treturn trans\n\t},\n\t\"timeFormatISO\": func(tz string, timestamp int64) string {\n\t\t_, _ = time.LoadLocation(tz)\n\t\treturn time.Unix(timestamp, 0).Format(\"2006-01-02T15:04:05.000Z\")\n\t},\n\t\"translatorTimeFormatLongDate\": func(la i18n.Language, tz string, timestamp int64) string {\n\t\ttrans := translator.GlobalTrans.Tr(la, \"ui.dates.long_date_with_time\")\n\t\treturn day.Format(timestamp, trans, tz)\n\t},\n\t\"translatorTimeFormat\": func(la i18n.Language, tz string, timestamp int64) string {\n\t\tvar (\n\t\t\tnow           = time.Now().Unix()\n\t\t\tbetween int64 = 0\n\t\t\ttrans   string\n\t\t)\n\t\t_, _ = time.LoadLocation(tz)\n\t\tif now > timestamp {\n\t\t\tbetween = now - timestamp\n\t\t}\n\n\t\tif between <= 1 {\n\t\t\treturn translator.GlobalTrans.Tr(la, \"ui.dates.now\")\n\t\t}\n\n\t\tif between > 1 && between < 60 {\n\t\t\ttrans = translator.GlobalTrans.Tr(la, \"ui.dates.x_seconds_ago\")\n\t\t\treturn strings.ReplaceAll(trans, \"{{count}}\", converter.IntToString(between))\n\t\t}\n\n\t\tif between >= 60 && between < 3600 {\n\t\t\tmin := between / 60\n\t\t\ttrans = translator.GlobalTrans.Tr(la, \"ui.dates.x_minutes_ago\")\n\t\t\treturn strings.ReplaceAll(trans, \"{{count}}\", strconv.FormatInt(min, 10))\n\t\t}\n\n\t\tif between >= 3600 && between < 3600*24 {\n\t\t\th := between / 3600\n\t\t\ttrans = translator.GlobalTrans.Tr(la, \"ui.dates.x_hours_ago\")\n\t\t\treturn strings.ReplaceAll(trans, \"{{count}}\", strconv.FormatInt(h, 10))\n\t\t}\n\n\t\tif between >= 3600*24 &&\n\t\t\tbetween < 3600*24*366 &&\n\t\t\ttime.Unix(timestamp, 0).Format(\"2006\") == time.Unix(now, 0).Format(\"2006\") {\n\t\t\ttrans = translator.GlobalTrans.Tr(la, \"ui.dates.long_date\")\n\t\t\treturn day.Format(timestamp, trans, tz)\n\t\t}\n\n\t\ttrans = translator.GlobalTrans.Tr(la, \"ui.dates.long_date_with_year\")\n\t\treturn day.Format(timestamp, trans, tz)\n\t},\n\t\"wrapComments\": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]any {\n\t\treturn map[string]any{\n\t\t\t\"comments\": comments,\n\t\t\t\"language\": la,\n\t\t\t\"timezone\": tz,\n\t\t}\n\t},\n\t\"urlTitle\": htmltext.UrlTitle,\n}\n\nfunc FormatLinkNofollow(html string) string {\n\tvar hrefRegexp = regexp.MustCompile(\"(?m)<a.*?[^<]>.*?</a>\")\n\tmatch := hrefRegexp.FindAllString(html, -1)\n\tfor _, v := range match {\n\t\thasNofollow := strings.Contains(v, \"rel=\\\"nofollow\\\"\")\n\t\thasSiteUrl := strings.Contains(v, controller.SiteUrl)\n\t\tif !hasSiteUrl {\n\t\t\tif !hasNofollow {\n\t\t\t\tnofollowUrl := strings.Replace(v, \"<a\", \"<a rel=\\\"nofollow\\\"\", 1)\n\t\t\t\thtml = strings.Replace(html, v, nofollowUrl, 1)\n\t\t\t}\n\t\t}\n\t}\n\treturn html\n}\n"
  },
  {
    "path": "internal/base/server/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage server\n\nimport \"github.com/google/wire\"\n\n// ProviderSetServer is providers.\nvar ProviderSetServer = wire.NewSet(NewHTTPServer)\n"
  },
  {
    "path": "internal/base/translator/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage translator\n\n// I18n i18n config\ntype I18n struct {\n\tBundleDir string `json:\"bundle_dir\" mapstructure:\"bundle_dir\" yaml:\"bundle_dir\"`\n}\n"
  },
  {
    "path": "internal/base/translator/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage translator\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/google/wire\"\n\tmyTran \"github.com/segmentfault/pacman/contrib/i18n\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// ProviderSet is providers.\nvar ProviderSet = wire.NewSet(NewTranslator)\nvar GlobalTrans i18n.Translator\n\n// LangOption language option\ntype LangOption struct {\n\tLabel string `json:\"label\"`\n\tValue string `json:\"value\"`\n\t// Translation completion percentage\n\tProgress int `json:\"progress\"`\n}\n\n// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.\nconst DefaultLangOption = \"Default\"\n\nvar (\n\t// LanguageOptions language\n\tLanguageOptions []*LangOption\n)\n\n// NewTranslator new a translator\nfunc NewTranslator(c *I18n) (tr i18n.Translator, err error) {\n\tentries, err := os.ReadDir(c.BundleDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// read the Bundle resources file from entries\n\tfor _, file := range entries {\n\t\t// ignore directory\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// ignore non-YAML file\n\t\tif filepath.Ext(file.Name()) != \".yaml\" && file.Name() != \"i18n.yaml\" {\n\t\t\tcontinue\n\t\t}\n\t\tlog.Debugf(\"try to read file: %s\", file.Name())\n\t\tbuf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"read file failed: %s %s\", file.Name(), err)\n\t\t}\n\n\t\t// parse the backend translation\n\t\toriginalTr := struct {\n\t\t\tBackend map[string]map[string]any `yaml:\"backend\"`\n\t\t\tUI      map[string]any            `yaml:\"ui\"`\n\t\t\tPlugin  map[string]any            `yaml:\"plugin\"`\n\t\t}{}\n\t\tif err = yaml.Unmarshal(buf, &originalTr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttranslation := make(map[string]any, 0)\n\t\tfor k, v := range originalTr.Backend {\n\t\t\ttranslation[k] = v\n\t\t}\n\t\ttranslation[\"backend\"] = originalTr.Backend\n\t\ttranslation[\"ui\"] = originalTr.UI\n\t\ttranslation[\"plugin\"] = originalTr.Plugin\n\n\t\tcontent, err := yaml.Marshal(translation)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"marshal translation content failed: %s %s\", file.Name(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// add translator use backend translation\n\t\tif err = myTran.AddTranslator(content, file.Name()); err != nil {\n\t\t\tlog.Debugf(\"add translator failed: %s %s\", file.Name(), err)\n\t\t\treportTranslatorFormatError(file.Name(), buf)\n\t\t\tcontinue\n\t\t}\n\t}\n\tGlobalTrans = myTran.GlobalTrans\n\n\ti18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, \"i18n.yaml\"))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read i18n file failed: %s\", err)\n\t}\n\n\ts := struct {\n\t\tLangOption []*LangOption `yaml:\"language_options\"`\n\t}{}\n\terr = yaml.Unmarshal(i18nFile, &s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"i18n file parsing failed: %s\", err)\n\t}\n\tLanguageOptions = s.LangOption\n\tfor _, option := range LanguageOptions {\n\t\toption.Label = fmt.Sprintf(\"%s (%d%%)\", option.Label, option.Progress)\n\t}\n\treturn GlobalTrans, err\n}\n\n// CheckLanguageIsValid check user input language is valid\nfunc CheckLanguageIsValid(lang string) bool {\n\tif lang == DefaultLangOption {\n\t\treturn true\n\t}\n\tfor _, option := range LanguageOptions {\n\t\tif option.Value == lang {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Tr use language to translate data. If this language translation is not available, return default english translation.\nfunc Tr(lang i18n.Language, data string) string {\n\tif GlobalTrans == nil {\n\t\treturn data\n\t}\n\ttranslation := GlobalTrans.Tr(lang, data)\n\tif translation == data {\n\t\treturn GlobalTrans.Tr(i18n.DefaultLanguage, data)\n\t}\n\treturn translation\n}\n\n// TrWithData translate key with template data, it will replace the template data {{ .PlaceHolder }} in the translation.\nfunc TrWithData(lang i18n.Language, key string, templateData any) string {\n\tif GlobalTrans == nil {\n\t\treturn key\n\t}\n\ttranslation := GlobalTrans.TrWithData(lang, key, templateData)\n\tif translation == key {\n\t\treturn GlobalTrans.TrWithData(i18n.DefaultLanguage, key, templateData)\n\t}\n\treturn translation\n}\n\n// reportTranslatorFormatError re-parses the YAML file to locate the invalid entry\n// when go-i18n fails to add the translator.\nfunc reportTranslatorFormatError(fileName string, content []byte) {\n\tvar raw any\n\tif err := yaml.Unmarshal(content, &raw); err != nil {\n\t\tlog.Errorf(\"parse translator file %s failed when diagnosing format error: %s\", fileName, err)\n\t\treturn\n\t}\n\tif err := inspectTranslatorNode(raw, nil, true); err != nil {\n\t\tlog.Errorf(\"translator file %s invalid: %s\", fileName, err)\n\t}\n}\n\nfunc inspectTranslatorNode(node any, path []string, isRoot bool) error {\n\tswitch data := node.(type) {\n\tcase nil:\n\t\tif isRoot {\n\t\t\treturn fmt.Errorf(\"root value is empty\")\n\t\t}\n\t\treturn fmt.Errorf(\"%s contains an empty value\", formatTranslationPath(path))\n\tcase string:\n\t\tif isRoot {\n\t\t\treturn fmt.Errorf(\"root value must be an object but found string\")\n\t\t}\n\t\treturn nil\n\tcase bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n\t\tif isRoot {\n\t\t\treturn fmt.Errorf(\"root value must be an object but found %T\", data)\n\t\t}\n\t\treturn fmt.Errorf(\"%s expects a string translation but found %T\", formatTranslationPath(path), data)\n\tcase map[string]any:\n\t\tif isMessageMap(data) {\n\t\t\treturn nil\n\t\t}\n\t\tkeys := make([]string, 0, len(data))\n\t\tfor key := range data {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\tsort.Strings(keys)\n\t\tfor _, key := range keys {\n\t\t\tif err := inspectTranslatorNode(data[key], append(path, key), false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase map[string]string:\n\t\tmapped := make(map[string]any, len(data))\n\t\tfor k, v := range data {\n\t\t\tmapped[k] = v\n\t\t}\n\t\treturn inspectTranslatorNode(mapped, path, isRoot)\n\tcase map[any]any:\n\t\tif isMessageMap(data) {\n\t\t\treturn nil\n\t\t}\n\t\ttype kv struct {\n\t\t\tkey string\n\t\t\tval any\n\t\t}\n\t\titems := make([]kv, 0, len(data))\n\t\tfor key, val := range data {\n\t\t\tstrKey, ok := key.(string)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"%s uses a non-string key %#v\", formatTranslationPath(path), key)\n\t\t\t}\n\t\t\titems = append(items, kv{key: strKey, val: val})\n\t\t}\n\t\tsort.Slice(items, func(i, j int) bool {\n\t\t\treturn items[i].key < items[j].key\n\t\t})\n\t\tfor _, item := range items {\n\t\t\tif err := inspectTranslatorNode(item.val, append(path, item.key), false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase []any:\n\t\tfor idx, child := range data {\n\t\t\tif err := inspectTranslatorNode(child, append(path, fmt.Sprintf(\"[%d]\", idx)), false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase []map[string]any:\n\t\tfor idx, child := range data {\n\t\t\tif err := inspectTranslatorNode(child, append(path, fmt.Sprintf(\"[%d]\", idx)), false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\tif isRoot {\n\t\t\treturn fmt.Errorf(\"root value must be an object but found %T\", data)\n\t\t}\n\t\treturn fmt.Errorf(\"%s contains unsupported value type %T\", formatTranslationPath(path), data)\n\t}\n}\n\nvar translatorReservedKeys = []string{\n\t\"id\", \"description\", \"hash\", \"leftdelim\", \"rightdelim\",\n\t\"zero\", \"one\", \"two\", \"few\", \"many\", \"other\",\n}\n\nfunc isMessageMap(data any) bool {\n\tswitch v := data.(type) {\n\tcase map[string]any:\n\t\tfor _, key := range translatorReservedKeys {\n\t\t\tval, ok := v[key]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := val.(string); ok {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\tcase map[string]string:\n\t\tfor _, key := range translatorReservedKeys {\n\t\t\tval, ok := v[key]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif val != \"\" {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\tcase map[any]any:\n\t\tfor _, key := range translatorReservedKeys {\n\t\t\tval, ok := v[key]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := val.(string); ok {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc formatTranslationPath(path []string) string {\n\tif len(path) == 0 {\n\t\treturn \"root\"\n\t}\n\tvar b strings.Builder\n\tfor _, part := range path {\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif part[0] == '[' {\n\t\t\tb.WriteString(part)\n\t\t\tcontinue\n\t\t}\n\t\tif b.Len() > 0 {\n\t\t\tb.WriteByte('.')\n\t\t}\n\t\tb.WriteString(part)\n\t}\n\treturn b.String()\n}\n"
  },
  {
    "path": "internal/base/validator/validator.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage validator\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/go-playground/locales\"\n\tgerman \"github.com/go-playground/locales/de\"\n\tenglish \"github.com/go-playground/locales/en\"\n\tspanish \"github.com/go-playground/locales/es\"\n\tfrench \"github.com/go-playground/locales/fr\"\n\titalian \"github.com/go-playground/locales/it\"\n\tjapanese \"github.com/go-playground/locales/ja\"\n\tkorean \"github.com/go-playground/locales/ko\"\n\tportuguese \"github.com/go-playground/locales/pt\"\n\trussian \"github.com/go-playground/locales/ru\"\n\tvietnamese \"github.com/go-playground/locales/vi\"\n\tchinese \"github.com/go-playground/locales/zh\"\n\tchineseTraditional \"github.com/go-playground/locales/zh_Hant_TW\"\n\tut \"github.com/go-playground/universal-translator\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/go-playground/validator/v10/translations/en\"\n\t\"github.com/go-playground/validator/v10/translations/es\"\n\t\"github.com/go-playground/validator/v10/translations/fr\"\n\t\"github.com/go-playground/validator/v10/translations/it\"\n\t\"github.com/go-playground/validator/v10/translations/ja\"\n\t\"github.com/go-playground/validator/v10/translations/pt\"\n\t\"github.com/go-playground/validator/v10/translations/ru\"\n\t\"github.com/go-playground/validator/v10/translations/vi\"\n\t\"github.com/go-playground/validator/v10/translations/zh\"\n\t\"github.com/go-playground/validator/v10/translations/zh_tw\"\n\t\"github.com/microcosm-cc/bluemonday\"\n\tmyErrors \"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype TranslatorLocal struct {\n\tLa           i18n.Language\n\tLo           locales.Translator\n\tRegisterFunc func(v *validator.Validate, trans ut.Translator) (err error)\n}\n\nvar (\n\tallLanguageTranslators = []*TranslatorLocal{\n\t\t{La: i18n.LanguageChinese, Lo: chinese.New(), RegisterFunc: zh.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageChineseTraditional, Lo: chineseTraditional.New(), RegisterFunc: zh_tw.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageEnglish, Lo: english.New(), RegisterFunc: en.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageGerman, Lo: german.New(), RegisterFunc: nil},\n\t\t{La: i18n.LanguageSpanish, Lo: spanish.New(), RegisterFunc: es.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageFrench, Lo: french.New(), RegisterFunc: fr.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageItalian, Lo: italian.New(), RegisterFunc: it.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageJapanese, Lo: japanese.New(), RegisterFunc: ja.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageKorean, Lo: korean.New(), RegisterFunc: nil},\n\t\t{La: i18n.LanguagePortuguese, Lo: portuguese.New(), RegisterFunc: pt.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageRussian, Lo: russian.New(), RegisterFunc: ru.RegisterDefaultTranslations},\n\t\t{La: i18n.LanguageVietnamese, Lo: vietnamese.New(), RegisterFunc: vi.RegisterDefaultTranslations},\n\t}\n)\n\n// MyValidator my validator\ntype MyValidator struct {\n\tValidate *validator.Validate\n\tTran     ut.Translator\n\tLang     i18n.Language\n}\n\n// FormErrorField indicates the current form error content. which field is error and error message.\ntype FormErrorField struct {\n\tErrorField string `json:\"error_field\"`\n\tErrorMsg   string `json:\"error_msg\"`\n}\n\n// GlobalValidatorMapping is a mapping from validator to translator used\nvar GlobalValidatorMapping = make(map[i18n.Language]*MyValidator, 0)\n\nfunc init() {\n\tfor _, t := range allLanguageTranslators {\n\t\ttran, val := getTran(t.Lo), createDefaultValidator(t.La)\n\t\tif t.RegisterFunc != nil {\n\t\t\tif err := t.RegisterFunc(val, tran); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\tGlobalValidatorMapping[t.La] = &MyValidator{Validate: val, Tran: tran, Lang: t.La}\n\t}\n}\n\nfunc getTran(lo locales.Translator) ut.Translator {\n\ttran, ok := ut.New(lo, lo).GetTranslator(lo.Locale())\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"not found translator %s\", lo.Locale()))\n\t}\n\treturn tran\n}\n\nfunc NotBlank(fl validator.FieldLevel) (res bool) {\n\tfield := fl.Field()\n\tswitch field.Kind() {\n\tcase reflect.String:\n\t\ttrimSpace := strings.TrimSpace(field.String())\n\t\tres := len(trimSpace) > 0\n\t\tif !res {\n\t\t\tfield.SetString(trimSpace)\n\t\t}\n\t\treturn true\n\tcase reflect.Chan, reflect.Map, reflect.Slice, reflect.Array:\n\t\treturn field.Len() > 0\n\tcase reflect.Ptr, reflect.Interface, reflect.Func:\n\t\treturn !field.IsNil()\n\tdefault:\n\t\treturn field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()\n\t}\n}\n\nfunc Sanitizer(fl validator.FieldLevel) (res bool) {\n\tfield := fl.Field()\n\tswitch field.Kind() {\n\tcase reflect.String:\n\t\tfilter := bluemonday.UGCPolicy()\n\t\tcontent := strings.ReplaceAll(filter.Sanitize(field.String()), \"&amp;\", \"&\")\n\t\tfield.SetString(content)\n\t\treturn true\n\tcase reflect.Chan, reflect.Map, reflect.Slice, reflect.Array:\n\t\treturn field.Len() > 0\n\tcase reflect.Ptr, reflect.Interface, reflect.Func:\n\t\treturn !field.IsNil()\n\tdefault:\n\t\treturn field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()\n\t}\n}\n\nfunc createDefaultValidator(la i18n.Language) *validator.Validate {\n\tvalidate := validator.New()\n\t// _ = validate.RegisterValidation(\"notblank\", validators.NotBlank)\n\t_ = validate.RegisterValidation(\"notblank\", NotBlank)\n\t_ = validate.RegisterValidation(\"sanitizer\", Sanitizer)\n\tvalidate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {\n\t\tdefer func() {\n\t\t\tif len(res) > 0 {\n\t\t\t\tres = translator.Tr(la, res)\n\t\t\t}\n\t\t}()\n\t\tif jsonTag := fld.Tag.Get(\"json\"); len(jsonTag) > 0 {\n\t\t\tif jsonTag == \"-\" {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn jsonTag\n\t\t}\n\t\tif formTag := fld.Tag.Get(\"form\"); len(formTag) > 0 {\n\t\t\treturn formTag\n\t\t}\n\t\treturn fld.Name\n\t})\n\treturn validate\n}\n\nfunc GetValidatorByLang(lang i18n.Language) *MyValidator {\n\tif GlobalValidatorMapping[lang] != nil {\n\t\treturn GlobalValidatorMapping[lang]\n\t}\n\treturn GlobalValidatorMapping[i18n.DefaultLanguage]\n}\n\n// Check /\nfunc (m *MyValidator) Check(value any) (errFields []*FormErrorField, err error) {\n\tdefer func() {\n\t\tif len(errFields) == 0 {\n\t\t\treturn\n\t\t}\n\t\tfor _, field := range errFields {\n\t\t\tif len(field.ErrorField) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfirstRune := []rune(field.ErrorMsg)[0]\n\t\t\tif !unicode.IsLetter(firstRune) || !unicode.Is(unicode.Latin, firstRune) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tupperFirstRune := unicode.ToUpper(firstRune)\n\t\t\tfield.ErrorMsg = string(upperFirstRune) + field.ErrorMsg[1:]\n\t\t\tif !strings.HasSuffix(field.ErrorMsg, \".\") {\n\t\t\t\tfield.ErrorMsg += \".\"\n\t\t\t}\n\t\t}\n\t}()\n\terr = m.Validate.Struct(value)\n\tif err != nil {\n\t\tvar valErrors validator.ValidationErrors\n\t\tif !errors.As(err, &valErrors) {\n\t\t\tlog.Error(err)\n\t\t\treturn nil, errors.New(\"validate check exception\")\n\t\t}\n\n\t\tfor _, fieldError := range valErrors {\n\t\t\terrField := &FormErrorField{\n\t\t\t\tErrorField: fieldError.Field(),\n\t\t\t\tErrorMsg:   fieldError.Translate(m.Tran),\n\t\t\t}\n\n\t\t\t// get original tag name from value for set err field key.\n\t\t\tstructNamespace := fieldError.StructNamespace()\n\t\t\t_, fieldName, found := strings.Cut(structNamespace, \".\")\n\t\t\tif found {\n\t\t\t\toriginalTag := getObjectTagByFieldName(value, fieldName)\n\t\t\t\tif len(originalTag) > 0 {\n\t\t\t\t\terrField.ErrorField = originalTag\n\t\t\t\t}\n\t\t\t}\n\t\t\terrFields = append(errFields, errField)\n\t\t}\n\t\tif len(errFields) > 0 {\n\t\t\terrMsg := \"\"\n\t\t\tif len(errFields) == 1 {\n\t\t\t\terrMsg = errFields[0].ErrorMsg\n\t\t\t}\n\t\t\treturn errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t\t}\n\t}\n\n\tif v, ok := value.(Checker); ok {\n\t\terrFields, err = v.Check()\n\t\tif err == nil {\n\t\t\treturn nil, nil\n\t\t}\n\t\terrMsg := \"\"\n\t\tfor _, errField := range errFields {\n\t\t\terrField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)\n\t\t\terrMsg = errField.ErrorMsg\n\t\t}\n\t\treturn errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t}\n\treturn nil, nil\n}\n\n// Checker .\ntype Checker interface {\n\tCheck() (errField []*FormErrorField, err error)\n}\n\nfunc getObjectTagByFieldName(obj any, fieldName string) (tag string) {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}()\n\n\tobjT := reflect.TypeOf(obj)\n\tobjT = objT.Elem()\n\n\tstructField, exists := objT.FieldByName(fieldName)\n\tif !exists {\n\t\treturn \"\"\n\t}\n\ttag = structField.Tag.Get(\"json\")\n\tif len(tag) == 0 {\n\t\treturn structField.Tag.Get(\"form\")\n\t}\n\treturn tag\n}\n"
  },
  {
    "path": "internal/cli/build.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/pkg/writer\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst (\n\tmainGoTpl = `package main\n\nimport (\n\tanswercmd \"github.com/apache/answer/cmd\"\n\n  // remote plugins\n\t{{- range .remote_plugins}}\n\t_ \"{{.}}\"\n\t{{- end}}\n\n  // local plugins\n\t{{- range .local_plugins}}\n\t_ \"answer/{{.}}\"\n\t{{- end}}\n)\n\nfunc main() {\n\tanswercmd.Main()\n}\n`\n\tgoModTpl = `module answer\n\ngo 1.23\n`\n)\n\ntype answerBuilder struct {\n\tbuildingMaterial *buildingMaterial\n\tBuildError       error\n}\n\ntype buildingMaterial struct {\n\tanswerModuleReplacement string\n\tplugins                 []*pluginInfo\n\toutputPath              string\n\ttmpDir                  string\n\toriginalAnswerInfo      OriginalAnswerInfo\n}\n\ntype OriginalAnswerInfo struct {\n\tVersion  string\n\tRevision string\n\tTime     string\n}\n\ntype pluginInfo struct {\n\t// Name of the plugin e.g. github.com/apache/answer-plugins/github-connector\n\tName string\n\t// Path to the plugin. If path exist, read plugin from local filesystem\n\tPath string\n\t// Version of the plugin\n\tVersion string\n}\n\nfunc newAnswerBuilder(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) *answerBuilder {\n\tmaterial := &buildingMaterial{originalAnswerInfo: originalAnswerInfo}\n\tparentDir, _ := filepath.Abs(\".\")\n\tif buildDir != \"\" {\n\t\tmaterial.tmpDir = filepath.Join(parentDir, buildDir)\n\t} else {\n\t\tmaterial.tmpDir, _ = os.MkdirTemp(parentDir, \"answer_build\")\n\t}\n\tif len(outputPath) == 0 {\n\t\toutputPath = filepath.Join(parentDir, \"new_answer\")\n\t}\n\tmaterial.outputPath, _ = filepath.Abs(outputPath)\n\tmaterial.plugins = formatPlugins(plugins)\n\tmaterial.answerModuleReplacement = os.Getenv(\"ANSWER_MODULE\")\n\treturn &answerBuilder{\n\t\tbuildingMaterial: material,\n\t}\n}\n\nfunc (a *answerBuilder) DoTask(task func(b *buildingMaterial) error) {\n\tif a.BuildError != nil {\n\t\treturn\n\t}\n\ta.BuildError = task(a.buildingMaterial)\n}\n\n// BuildNewAnswer builds a new answer with specified plugins\nfunc BuildNewAnswer(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) (err error) {\n\tbuilder := newAnswerBuilder(buildDir, outputPath, plugins, originalAnswerInfo)\n\tbuilder.DoTask(createMainGoFile)\n\tbuilder.DoTask(downloadGoModFile)\n\tbuilder.DoTask(movePluginToVendor)\n\tbuilder.DoTask(copyUIFiles)\n\tbuilder.DoTask(buildUI)\n\tbuilder.DoTask(mergeI18nFiles)\n\tbuilder.DoTask(buildBinary)\n\tbuilder.DoTask(cleanByproduct)\n\treturn builder.BuildError\n}\n\nfunc formatPlugins(plugins []string) (formatted []*pluginInfo) {\n\tfor _, plugin := range plugins {\n\t\tplugin = strings.TrimSpace(plugin)\n\t\t// plugin description like this 'github.com/apache/answer-plugins/github-connector@latest=/local/path'\n\t\tinfo := &pluginInfo{}\n\t\tplugin, info.Path, _ = strings.Cut(plugin, \"=\")\n\t\tinfo.Name, info.Version, _ = strings.Cut(plugin, \"@\")\n\t\tformatted = append(formatted, info)\n\t}\n\treturn formatted\n}\n\n// createMainGoFile creates main.go file in tmp dir that content is mainGoTpl\nfunc createMainGoFile(b *buildingMaterial) (err error) {\n\tfmt.Printf(\"[build] build dir: %s\\n\", b.tmpDir)\n\terr = dir.CreateDirIfNotExist(b.tmpDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tremotePlugins []string\n\t)\n\tfor _, p := range b.plugins {\n\t\tremotePlugins = append(remotePlugins, versionedModulePath(p.Name, p.Version))\n\t}\n\n\tmainGoFile := &bytes.Buffer{}\n\ttmpl, err := template.New(\"main\").Parse(mainGoTpl)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = tmpl.Execute(mainGoFile, map[string]any{\n\t\t\"remote_plugins\": remotePlugins,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = writer.WriteFile(filepath.Join(b.tmpDir, \"main.go\"), mainGoFile.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = writer.WriteFile(filepath.Join(b.tmpDir, \"go.mod\"), goModTpl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, p := range b.plugins {\n\t\t// If user set a path, use it to replace the module with local path\n\t\tif len(p.Path) > 0 {\n\t\t\treplacement := fmt.Sprintf(\"%s@%s=%s\", p.Name, p.Version, p.Path)\n\t\t\terr = b.newExecCmd(\"go\", \"mod\", \"edit\", \"-replace\", replacement).Run()\n\t\t} else if len(p.Version) > 0 {\n\t\t\t// If user specify a version, use it to get specific version of the module\n\t\t\terr = b.newExecCmd(\"go\", \"get\", fmt.Sprintf(\"%s@%s\", p.Name, p.Version)).Run()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn\n}\n\n// downloadGoModFile run go mod commands to download dependencies\nfunc downloadGoModFile(b *buildingMaterial) (err error) {\n\t// If user specify a module replacement, use it. Otherwise, use the latest version.\n\tif len(b.answerModuleReplacement) > 0 {\n\t\treplacement := fmt.Sprintf(\"%s=%s\", \"github.com/apache/answer\", b.answerModuleReplacement)\n\t\terr = b.newExecCmd(\"go\", \"mod\", \"edit\", \"-replace\", replacement).Run()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = b.newExecCmd(\"go\", \"mod\", \"tidy\").Run()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = b.newExecCmd(\"go\", \"mod\", \"vendor\").Run()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn\n}\n\n// movePluginToVendor move plugin to vendor dir\n// Traverse the plugins, and if the plugin path is not github.com/apache/answer-plugins, move the contents of the current plugin to the vendor/github.com/apache/answer-plugins/ directory.\nfunc movePluginToVendor(b *buildingMaterial) (err error) {\n\tpluginsDir := filepath.Join(b.tmpDir, \"vendor/github.com/apache/answer-plugins/\")\n\tfor _, p := range b.plugins {\n\t\tpluginDir := filepath.Join(b.tmpDir, \"vendor/\", p.Name)\n\t\tpluginName := filepath.Base(p.Name)\n\t\tif !strings.HasPrefix(p.Name, \"github.com/apache/answer-plugins/\") {\n\t\t\tfmt.Printf(\"try to copy dir from %s to %s\\n\", pluginDir, filepath.Join(pluginsDir, pluginName))\n\t\t\terr = copyDirEntries(os.DirFS(pluginDir), \".\", filepath.Join(pluginsDir, pluginName), \"node_modules\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// copyUIFiles copy ui files from answer module to tmp dir\nfunc copyUIFiles(b *buildingMaterial) (err error) {\n\tgoListCmd := b.newExecCmd(\"go\", \"list\", \"-mod=mod\", \"-m\", \"-f\", \"{{.Dir}}\", \"github.com/apache/answer\")\n\tbuf := new(bytes.Buffer)\n\tgoListCmd.Stdout = buf\n\tif err = goListCmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run go list: %w\", err)\n\t}\n\n\tanswerDir := strings.TrimSpace(buf.String())\n\tgoModUIDir := filepath.Join(answerDir, \"ui\")\n\tlocalUIBuildDir := filepath.Join(b.tmpDir, \"vendor/github.com/apache/answer/ui/\")\n\t// The node_modules folder generated during development will interfere packaging, so it needs to be ignored.\n\tif err = copyDirEntries(os.DirFS(goModUIDir), \".\", localUIBuildDir, \"node_modules\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy ui files: %w\", err)\n\t}\n\n\tpluginsDir := filepath.Join(b.tmpDir, \"vendor/github.com/apache/answer-plugins/\")\n\tlocalUIPluginDir := filepath.Join(localUIBuildDir, \"src/plugins/\")\n\n\t// copy plugins dir\n\tfmt.Printf(\"try to copy dir from %s to %s\\n\", pluginsDir, localUIPluginDir)\n\n\t// if plugins dir not exist means no plugins\n\tif !dir.CheckDirExist(pluginsDir) {\n\t\treturn nil\n\t}\n\n\tpluginsDirEntries, err := os.ReadDir(pluginsDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read plugins dir: %w\", err)\n\t}\n\tfor _, entry := range pluginsDirEntries {\n\t\tif !entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tsourcePluginDir := filepath.Join(pluginsDir, entry.Name())\n\t\t// check if plugin is a ui plugin\n\t\tpackageJsonPath := filepath.Join(sourcePluginDir, \"package.json\")\n\t\tfmt.Printf(\"check if %s is a ui plugin\\n\", packageJsonPath)\n\t\tif !dir.CheckFileExist(packageJsonPath) {\n\t\t\tcontinue\n\t\t}\n\n\t\tpnpmInstallCmd := b.newExecCmd(\"pnpm\", \"install\")\n\t\tpnpmInstallCmd.Dir = sourcePluginDir\n\t\tif err = pnpmInstallCmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to install plugin dependencies: %w\", err)\n\t\t}\n\n\t\tlocalPluginDir := filepath.Join(localUIPluginDir, entry.Name())\n\t\tfmt.Printf(\"try to copy dir from %s to %s\\n\", sourcePluginDir, localPluginDir)\n\t\tif err = copyDirEntries(os.DirFS(sourcePluginDir), \".\", localPluginDir, \"node_modules\"); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to copy ui files: %w\", err)\n\t\t}\n\t}\n\tformatUIPluginsDirName(localUIPluginDir)\n\treturn nil\n}\n\n// buildUI run pnpm install and pnpm build commands to build ui\nfunc buildUI(b *buildingMaterial) (err error) {\n\tlocalUIBuildDir := filepath.Join(b.tmpDir, \"vendor/github.com/apache/answer/ui\")\n\n\tpnpmInstallCmd := b.newExecCmd(\"pnpm\", \"pre-install\")\n\tpnpmInstallCmd.Dir = localUIBuildDir\n\tif err = pnpmInstallCmd.Run(); err != nil {\n\t\treturn err\n\t}\n\n\tpnpmBuildCmd := b.newExecCmd(\"pnpm\", \"build\")\n\tpnpmBuildCmd.Dir = localUIBuildDir\n\tif err = pnpmBuildCmd.Run(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// mergeI18nFiles merge i18n files\nfunc mergeI18nFiles(b *buildingMaterial) (err error) {\n\tfmt.Printf(\"try to merge i18n files\\n\")\n\n\ttype YamlPluginContent struct {\n\t\tPlugin map[string]any `yaml:\"plugin\"`\n\t}\n\n\tpluginAllTranslations := make(map[string]*YamlPluginContent)\n\tfor _, plugin := range b.plugins {\n\t\ti18nDir := filepath.Join(b.tmpDir, fmt.Sprintf(\"vendor/%s/i18n\", plugin.Name))\n\t\tfmt.Println(\"i18n dir: \", i18nDir)\n\t\tif !dir.CheckDirExist(i18nDir) {\n\t\t\tcontinue\n\t\t}\n\n\t\tentries, err := os.ReadDir(i18nDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, file := range entries {\n\t\t\t// ignore directory\n\t\t\tif file.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// ignore non-YAML file\n\t\t\tif filepath.Ext(file.Name()) != \".yaml\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbuf, err := os.ReadFile(filepath.Join(i18nDir, file.Name()))\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"read translation file failed: %s %s\", file.Name(), err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttranslation := &YamlPluginContent{}\n\t\t\tif err = yaml.Unmarshal(buf, translation); err != nil {\n\t\t\t\tlog.Debugf(\"unmarshal translation file failed: %s %s\", file.Name(), err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif pluginAllTranslations[file.Name()] == nil {\n\t\t\t\tpluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)}\n\t\t\t}\n\t\t\tfor k, v := range translation.Plugin {\n\t\t\t\tpluginAllTranslations[file.Name()].Plugin[k] = v\n\t\t\t}\n\t\t}\n\t}\n\n\toriginalI18nDir := filepath.Join(b.tmpDir, \"vendor/github.com/apache/answer/i18n\")\n\tentries, err := os.ReadDir(originalI18nDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range entries {\n\t\t// ignore directory\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// ignore non-YAML file\n\t\tfilename := file.Name()\n\t\tif filepath.Ext(filename) != \".yaml\" && filename != \"i18n.yaml\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// if plugin don't have this translation file, ignore it\n\t\tif pluginAllTranslations[filename] == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tout, _ := yaml.Marshal(pluginAllTranslations[filename])\n\n\t\tbuf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"read translation file failed: %s %s\", filename, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t_, _ = buf.WriteString(\"\\n\")\n\t\t_, _ = buf.Write(out)\n\t\t_ = buf.Close()\n\t}\n\treturn err\n}\n\nfunc copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...string) (err error) {\n\terr = dir.CreateDirIfNotExist(targetDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tignoreThisDir := func(path string) bool {\n\t\tfor _, s := range ignoreDir {\n\t\t\tif strings.HasPrefix(path, s) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\terr = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif ignoreThisDir(path) {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes\n\t\tpath = filepath.ToSlash(path)\n\n\t\t// Construct the absolute path for the source file/directory\n\t\tsrcPath := filepath.Join(sourceDir, path)\n\t\tsrcPath = filepath.ToSlash(srcPath)\n\n\t\t// Construct the absolute path for the destination file/directory\n\t\tdstPath := filepath.Join(targetDir, path)\n\n\t\tif d.IsDir() {\n\t\t\t// Create the directory in the destination\n\t\t\terr := os.MkdirAll(dstPath, os.ModePerm)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create directory %s: %w\", dstPath, err)\n\t\t\t}\n\t\t} else {\n\t\t\t// Open the source file\n\t\t\tsrcFile, err := sourceFs.Open(srcPath)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to open source file %s: %w\", srcPath, err)\n\t\t\t}\n\t\t\tdefer srcFile.Close()\n\n\t\t\t// Create the destination file\n\t\t\tdstFile, err := os.Create(dstPath)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create destination file %s: %w\", dstPath, err)\n\t\t\t}\n\t\t\tdefer dstFile.Close()\n\n\t\t\t// Copy the file contents\n\t\t\t_, err = io.Copy(dstFile, srcFile)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to copy file contents from %s to %s: %w\", srcPath, dstPath, err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn err\n}\n\n// format plugins dir name from dash to underline\nfunc formatUIPluginsDirName(dirPath string) {\n\tentries, err := os.ReadDir(dirPath)\n\tif err != nil {\n\t\tfmt.Printf(\"read ui plugins dir failed: [%s] %s\\n\", dirPath, err)\n\t\treturn\n\t}\n\tfor _, entry := range entries {\n\t\tif !entry.IsDir() || !strings.Contains(entry.Name(), \"-\") {\n\t\t\tcontinue\n\t\t}\n\t\tnewName := strings.ReplaceAll(entry.Name(), \"-\", \"_\")\n\t\tif err := os.Rename(filepath.Join(dirPath, entry.Name()), filepath.Join(dirPath, newName)); err != nil {\n\t\t\tfmt.Printf(\"rename ui plugins dir failed: [%s] %s\\n\", dirPath, err)\n\t\t} else {\n\t\t\tfmt.Printf(\"rename ui plugins dir success: [%s] -> [%s]\\n\", entry.Name(), newName)\n\t\t}\n\t}\n}\n\n// buildBinary build binary file\nfunc buildBinary(b *buildingMaterial) (err error) {\n\tversionInfo := b.originalAnswerInfo\n\tcmdPkg := \"github.com/apache/answer/cmd\"\n\tldflags := fmt.Sprintf(\"-X %s.Version=%s -X %s.Revision=%s -X %s.Time=%s\",\n\t\tcmdPkg, versionInfo.Version, cmdPkg, versionInfo.Revision, cmdPkg, versionInfo.Time)\n\terr = b.newExecCmd(\"go\", \"build\",\n\t\t\"-ldflags\", ldflags, \"-o\", b.outputPath, \".\").Run()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn\n}\n\n// cleanByproduct delete tmp dir\nfunc cleanByproduct(b *buildingMaterial) (err error) {\n\treturn os.RemoveAll(b.tmpDir)\n}\n\nfunc (b *buildingMaterial) newExecCmd(command string, args ...string) *exec.Cmd {\n\tcmd := exec.Command(command, args...)\n\tfmt.Println(cmd.Args)\n\tcmd.Dir = b.tmpDir\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd\n}\n\nfunc versionedModulePath(modulePath, moduleVersion string) string {\n\tif moduleVersion == \"\" {\n\t\treturn modulePath\n\t}\n\tver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, \"v\"))\n\tif err != nil {\n\t\treturn modulePath\n\t}\n\tmajor := ver.Major()\n\tif major > 1 {\n\t\tmodulePath += fmt.Sprintf(\"/v%d\", major)\n\t}\n\treturn path.Clean(modulePath)\n}\n"
  },
  {
    "path": "internal/cli/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\ntype ConfigField struct {\n\tAllowPasswordLogin bool `json:\"allow_password_login\"`\n\t// The slug name of plugin that you want to deactivate\n\tDeactivatePluginSlugName string `json:\"deactivate_plugin_slug_name\"`\n}\n\n// SetDefaultConfig set default config\nfunc SetDefaultConfig(dbConf *data.Database, cacheConf *data.CacheConf, field *ConfigField) error {\n\tdb, err := data.NewDB(false, dbConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = db.Close()\n\t}()\n\n\tcache, cacheCleanup, err := data.NewCache(cacheConf)\n\tif err != nil {\n\t\tfmt.Println(\"new cache failed\")\n\t}\n\tdefer func() {\n\t\tif cache != nil {\n\t\t\t_ = cache.Flush(context.Background())\n\t\t\tcacheCleanup()\n\t\t}\n\t}()\n\n\tif field.AllowPasswordLogin {\n\t\treturn defaultLoginConfig(db)\n\t}\n\tif len(field.DeactivatePluginSlugName) > 0 {\n\t\treturn deactivatePlugin(db, field.DeactivatePluginSlugName)\n\t}\n\n\treturn nil\n}\n\nfunc defaultLoginConfig(x *xorm.Engine) (err error) {\n\tfmt.Println(\"set default login config\")\n\n\tloginSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeLogin,\n\t}\n\texist, err := x.Get(loginSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\tvar content map[string]any\n\t\t_ = json.Unmarshal([]byte(loginSiteInfo.Content), &content)\n\t\tcontent[\"allow_password_login\"] = true\n\t\tdataByte, _ := json.Marshal(content)\n\t\tloginSiteInfo.Content = string(dataByte)\n\t\t_, err = x.ID(loginSiteInfo.ID).Cols(\"content\").Update(loginSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc deactivatePlugin(x *xorm.Engine, pluginSlugName string) (err error) {\n\tfmt.Printf(\"try to deactivate plugin: %s\\n\", pluginSlugName)\n\n\titem := &entity.Config{Key: constant.PluginStatus}\n\texist, err := x.Get(item)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\n\tpluginStatusMapping := make(map[string]bool)\n\t_ = json.Unmarshal([]byte(item.Value), &pluginStatusMapping)\n\tstatus, ok := pluginStatusMapping[pluginSlugName]\n\tif !ok {\n\t\tfmt.Printf(\"plugin %s not exist\\n\", pluginSlugName)\n\t\treturn nil\n\t}\n\tif !status {\n\t\tfmt.Printf(\"plugin %s already deactivated\\n\", pluginSlugName)\n\t\treturn nil\n\t}\n\n\tpluginStatusMapping[pluginSlugName] = false\n\tdataByte, _ := json.Marshal(pluginStatusMapping)\n\titem.Value = string(dataByte)\n\t_, err = x.ID(item.ID).Cols(\"value\").Update(item)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"update plugin status failed: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/dump.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"xorm.io/xorm/schemas\"\n)\n\n// DumpAllData dump all database data to sql\nfunc DumpAllData(dataConf *data.Database, dumpDataPath string) error {\n\tdb, err := data.NewDB(false, dataConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = db.Close()\n\t}()\n\tif err = db.Ping(); err != nil {\n\t\treturn err\n\t}\n\n\tname := filepath.Join(dumpDataPath, fmt.Sprintf(\"answer_dump_data_%s.sql\", time.Now().Format(\"2006-01-02\")))\n\treturn db.DumpAllToFile(name, schemas.MYSQL)\n}\n"
  },
  {
    "path": "internal/cli/i18n.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/i18n\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/pkg/writer\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype YamlPluginContent struct {\n\tPlugin map[string]any `yaml:\"plugin\"`\n}\n\n// ReplaceI18nFilesLocal replace i18n files\nfunc ReplaceI18nFilesLocal(i18nDir string) error {\n\ti18nList, err := i18n.I18n.ReadDir(\".\")\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn err\n\t}\n\tfmt.Printf(\"[i18n] find i18n bundle %d\\n\", len(i18nList))\n\tfor _, item := range i18nList {\n\t\tpath := filepath.Join(i18nDir, item.Name())\n\t\tcontent, err := i18n.I18n.ReadFile(item.Name())\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\texist := dir.CheckFileExist(path)\n\t\tif exist {\n\t\t\tfmt.Printf(\"[i18n] install %s file exist, try to replace it\\n\", item.Name())\n\t\t\tif err = os.Remove(path); err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t}\n\t\t}\n\t\tfmt.Printf(\"[i18n] install %s bundle...\\n\", item.Name())\n\t\terr = writer.WriteFile(path, string(content))\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"[i18n] install %s bundle fail: %s\\n\", item.Name(), err.Error())\n\t\t} else {\n\t\t\tfmt.Printf(\"[i18n] install %s bundle success\\n\", item.Name())\n\t\t}\n\t}\n\treturn nil\n}\n\n// MergeI18nFilesLocal merge i18n files\nfunc MergeI18nFilesLocal(originalI18nDir, targetI18nDir string) (err error) {\n\tpluginAllTranslations := make(map[string]*YamlPluginContent)\n\n\terr = findI18nFileInDir(pluginAllTranslations, targetI18nDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tentries, err := os.ReadDir(originalI18nDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range entries {\n\t\t// ignore directory\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// ignore non-YAML file\n\t\tfilename := file.Name()\n\t\tif filepath.Ext(filename) != \".yaml\" && filename != \"i18n.yaml\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// if plugin don't have this translation file, ignore it\n\t\tif pluginAllTranslations[filename] == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tout, _ := yaml.Marshal(pluginAllTranslations[filename])\n\n\t\tbuf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"[i18n] read translation file failed: %s %s\\n\", filename, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t_, _ = buf.WriteString(\"\\n\")\n\t\t_, _ = buf.Write(out)\n\t\t_ = buf.Close()\n\t\tfmt.Printf(\"[i18n] merge i18n file: %s success\\n\", filename)\n\t}\n\n\treturn nil\n}\n\n// find i18n file in dir\nfunc findI18nFileInDir(pluginAllTranslations map[string]*YamlPluginContent, i18nDir string) error {\n\t// if i18n dir is not i18n, find deeper\n\tdirBase := filepath.Base(i18nDir)\n\tif dirBase != \"i18n\" {\n\t\tif strings.HasPrefix(dirBase, \".\") {\n\t\t\treturn nil\n\t\t}\n\t\t// find all i18n dir in target dir\n\t\ttargetDirs, err := os.ReadDir(i18nDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, targetDir := range targetDirs {\n\t\t\tif targetDir.IsDir() {\n\t\t\t\tif err := findI18nFileInDir(pluginAllTranslations, filepath.Join(i18nDir, targetDir.Name())); err != nil {\n\t\t\t\t\tfmt.Printf(\"[i18n] find i18n file in dir failed: %s %s\\n\", targetDir.Name(), err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfmt.Printf(\"[i18n] find i18n file in dir: %s\\n\", i18nDir)\n\n\t// if i18nDir is i18n, find all yaml files\n\tentries, err := os.ReadDir(i18nDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range entries {\n\t\t// ignore directory\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// ignore non-YAML file\n\t\tif filepath.Ext(file.Name()) != \".yaml\" {\n\t\t\tcontinue\n\t\t}\n\t\tbuf, err := os.ReadFile(filepath.Join(i18nDir, file.Name()))\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"[i18n] read translation file failed: %s %s\\n\", file.Name(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\ttranslation := &YamlPluginContent{}\n\t\tif err = yaml.Unmarshal(buf, translation); err != nil {\n\t\t\tfmt.Printf(\"[i18n] unmarshal translation file failed: %s %s\\n\", file.Name(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif pluginAllTranslations[file.Name()] == nil {\n\t\t\tpluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)}\n\t\t}\n\t\tfor k, v := range translation.Plugin {\n\t\t\tpluginAllTranslations[file.Name()].Plugin[k] = v\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/install.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/apache/answer/configs\"\n\t\"github.com/apache/answer/i18n\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/pkg/writer\"\n)\n\n// InstallAllInitialEnvironment install all initial environment\nfunc InstallAllInitialEnvironment(dataDirPath string) {\n\tpath.FormatAllPath(dataDirPath)\n\tinstallUploadDir()\n\tInstallI18nBundle(false)\n\tfmt.Println(\"install all initial environment done\")\n}\n\nfunc InstallConfigFile(configFilePath string) error {\n\tif len(configFilePath) == 0 {\n\t\tconfigFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName)\n\t}\n\tfmt.Println(\"[config-file] try to create at \", configFilePath)\n\n\t// if config file already exists do nothing.\n\tif CheckConfigFile(configFilePath) {\n\t\tfmt.Printf(\"[config-file] %s already exists\\n\", configFilePath)\n\t\treturn nil\n\t}\n\n\tif err := dir.CreateDirIfNotExist(path.ConfigFileDir); err != nil {\n\t\tfmt.Printf(\"[config-file] create directory fail %s\\n\", err.Error())\n\t\treturn fmt.Errorf(\"create directory fail %s\", err.Error())\n\t}\n\tfmt.Printf(\"[config-file] create directory success, config file is %s\\n\", configFilePath)\n\n\tif err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil {\n\t\tfmt.Printf(\"[config-file] install fail %s\\n\", err.Error())\n\t\treturn fmt.Errorf(\"write file failed %s\", err)\n\t}\n\tfmt.Printf(\"[config-file] install success\\n\")\n\treturn nil\n}\n\nfunc installUploadDir() {\n\tfmt.Println(\"[upload-dir] try to install...\")\n\tif err := dir.CreateDirIfNotExist(path.UploadFilePath); err != nil {\n\t\tfmt.Printf(\"[upload-dir] install fail %s\\n\", err.Error())\n\t} else {\n\t\tfmt.Printf(\"[upload-dir] install success, upload directory is %s\\n\", path.UploadFilePath)\n\t}\n}\n\nfunc InstallI18nBundle(replace bool) {\n\tfmt.Println(\"[i18n] try to install i18n bundle...\")\n\t// if SKIP_REPLACE_I18N is set, skip replace i18n bundles\n\tif len(os.Getenv(\"SKIP_REPLACE_I18N\")) > 0 {\n\t\treplace = false\n\t}\n\tif err := dir.CreateDirIfNotExist(path.I18nPath); err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\n\ti18nList, err := i18n.I18n.ReadDir(\".\")\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t\treturn\n\t}\n\tfmt.Printf(\"[i18n] find i18n bundle %d\\n\", len(i18nList))\n\tfor _, item := range i18nList {\n\t\tpath := filepath.Join(path.I18nPath, item.Name())\n\t\tcontent, err := i18n.I18n.ReadFile(item.Name())\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\texist := dir.CheckFileExist(path)\n\t\tif exist && !replace {\n\t\t\tcontinue\n\t\t}\n\t\tif exist {\n\t\t\tfmt.Printf(\"[i18n] install %s file exist, try to replace it\\n\", item.Name())\n\t\t\tif err = os.Remove(path); err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t}\n\t\t}\n\t\tfmt.Printf(\"[i18n] install %s bundle...\\n\", item.Name())\n\t\terr = writer.WriteFile(path, string(content))\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"[i18n] install %s bundle fail: %s\\n\", item.Name(), err.Error())\n\t\t} else {\n\t\t\tfmt.Printf(\"[i18n] install %s bundle success\\n\", item.Name())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/cli/install_check.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/pkg/dir\"\n)\n\nfunc CheckConfigFile(configPath string) bool {\n\treturn dir.CheckFileExist(configPath)\n}\n\nfunc CheckUploadDir() bool {\n\treturn dir.CheckDirExist(path.UploadFilePath)\n}\n\n// CheckDBConnection check database whether the connection is normal\nfunc CheckDBConnection(dataConf *data.Database) bool {\n\tdb, err := data.NewDB(false, dataConf)\n\tif err != nil {\n\t\tfmt.Printf(\"connection database failed: %s\\n\", err)\n\t\treturn false\n\t}\n\tdefer func() {\n\t\t_ = db.Close()\n\t}()\n\tif err = db.Ping(); err != nil {\n\t\tfmt.Printf(\"connection ping database failed: %s\\n\", err)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// CheckDBTableExist check database whether the table is already exists\nfunc CheckDBTableExist(dataConf *data.Database) bool {\n\tdb, err := data.NewDB(false, dataConf)\n\tif err != nil {\n\t\tfmt.Printf(\"connection database failed: %s\\n\", err)\n\t\treturn false\n\t}\n\tdefer func() {\n\t\t_ = db.Close()\n\t}()\n\tif err = db.Ping(); err != nil {\n\t\tfmt.Printf(\"connection ping database failed: %s\\n\", err)\n\t\treturn false\n\t}\n\n\texist, err := db.IsTableExist(&entity.Version{})\n\tif err != nil {\n\t\tfmt.Printf(\"check table exist failed: %s\\n\", err)\n\t\treturn false\n\t}\n\tif !exist {\n\t\tfmt.Printf(\"check table not exist\\n\")\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/cli/reset_password.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage cli\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/repo/api_key\"\n\t\"github.com/apache/answer/internal/repo/auth\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\tauthService \"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"golang.org/x/term\"\n\t_ \"modernc.org/sqlite\"\n\t\"xorm.io/xorm\"\n)\n\nconst (\n\tcharsetLower                = \"abcdefghijklmnopqrstuvwxyz\"\n\tcharsetUpper                = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\tcharsetDigits               = \"0123456789\"\n\tcharsetSpecial              = \"!@#$%^&*~?_-\"\n\tmaxRetries                  = 10\n\tdefaultRandomPasswordLength = 12\n)\n\nvar charset = []string{\n\tcharsetLower,\n\tcharsetUpper,\n\tcharsetDigits,\n\tcharsetSpecial,\n}\n\ntype ResetPasswordOptions struct {\n\tEmail    string\n\tPassword string\n}\n\nfunc ResetPassword(ctx context.Context, dataDirPath string, opts *ResetPasswordOptions) error {\n\tpath.FormatAllPath(dataDirPath)\n\n\tconfig, err := conf.ReadConfig(path.GetConfigFilePath())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read config file failed: %w\", err)\n\t}\n\n\tdb, err := initDatabase(config.Data.Database.Driver, config.Data.Database.Connection)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"connect database failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = db.Close()\n\t}()\n\n\tcache, cacheCleanup, err := data.NewCache(config.Data.Cache)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initialize cache failed: %w\", err)\n\t}\n\tdefer cacheCleanup()\n\n\tdataData, dataCleanup, err := data.NewData(db, cache)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initialize data layer failed: %w\", err)\n\t}\n\tdefer dataCleanup()\n\n\tuserRepo := user.NewUserRepo(dataData)\n\tauthRepo := auth.NewAuthRepo(dataData)\n\tapiKeyRepo := api_key.NewAPIKeyRepo(dataData)\n\tauthSvc := authService.NewAuthService(authRepo, apiKeyRepo)\n\n\temail := strings.TrimSpace(opts.Email)\n\tif email == \"\" {\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tfmt.Print(\"Please input user email: \")\n\t\temailInput, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"read email input failed: %w\", err)\n\t\t}\n\t\temail = strings.TrimSpace(emailInput)\n\t}\n\n\tuserInfo, exist, err := userRepo.GetByEmail(ctx, email)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"query user failed: %w\", err)\n\t}\n\tif !exist {\n\t\treturn fmt.Errorf(\"user not found: %s\", email)\n\t}\n\n\tfmt.Printf(\"You are going to reset password for user: %s\\n\", email)\n\n\tpassword := strings.TrimSpace(opts.Password)\n\n\tif password != \"\" {\n\t\tprintWarning(\"Passing password via command line may be recorded in shell history\")\n\t\tif err := checker.CheckPassword(password); err != nil {\n\t\t\treturn fmt.Errorf(\"password validation failed: %w\", err)\n\t\t}\n\t} else {\n\t\tpassword, err = promptForPassword()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"password input failed: %w\", err)\n\t\t}\n\t}\n\n\tif !confirmAction(fmt.Sprintf(\"This will reset password for user '[%s]%s'. Continue?\", userInfo.DisplayName, email)) {\n\t\tfmt.Println(\"Operation cancelled\")\n\t\treturn nil\n\t}\n\n\thashPwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"encrypt password failed: %w\", err)\n\t}\n\n\tif err = userRepo.UpdatePass(ctx, userInfo.ID, string(hashPwd)); err != nil {\n\t\treturn fmt.Errorf(\"update password failed: %w\", err)\n\t}\n\n\tauthSvc.RemoveUserAllTokens(ctx, userInfo.ID)\n\n\tfmt.Printf(\"Password has been successfully updated for user: %s\\n\", email)\n\tfmt.Println(\"All login sessions have been cleared\")\n\n\treturn nil\n}\n\n// promptForPassword prompts for a password\nfunc promptForPassword() (string, error) {\n\tfor {\n\t\tinput, err := getPasswordInput(\"Please input new password (empty to generate random password): \")\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif input == \"\" {\n\t\t\tpassword, err := generateRandomPasswordWithRetry()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"generate random password failed: %w\", err)\n\t\t\t}\n\t\t\tfmt.Printf(\"Generated random password: %s\\n\", password)\n\t\t\tfmt.Println(\"Please save this password in a secure location\")\n\t\t\treturn password, nil\n\t\t}\n\n\t\tif err := checker.CheckPassword(input); err != nil {\n\t\t\tfmt.Printf(\"Password validation failed: %v\\n\", err)\n\t\t\tfmt.Println(\"Please try again\")\n\t\t\tcontinue\n\t\t}\n\n\t\tconfirmPwd, err := getPasswordInput(\"Please confirm new password: \")\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif input != confirmPwd {\n\t\t\tfmt.Println(\"Passwords do not match, please try again\")\n\t\t\tcontinue\n\t\t}\n\n\t\treturn input, nil\n\t}\n}\n\nfunc generateRandomPasswordWithRetry() (string, error) {\n\tvar password string\n\tvar err error\n\n\tfor range maxRetries {\n\t\tpassword, err = generateRandomPassword(defaultRandomPasswordLength)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err := checker.CheckPassword(password); err == nil {\n\t\t\treturn password, nil\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn \"\", fmt.Errorf(\"failed to generate valid password after %d retries\", maxRetries)\n}\n\nfunc getPasswordInput(prompt string) (string, error) {\n\tfmt.Print(prompt)\n\tpassword, err := term.ReadPassword(int(os.Stdin.Fd()))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfmt.Println()\n\treturn string(password), nil\n}\n\nfunc generateRandomPassword(length int) (string, error) {\n\tif length < len(charset) {\n\t\treturn \"\", fmt.Errorf(\"password length must be at least %d\", len(charset))\n\t}\n\n\tbytes := make([]byte, length)\n\tfor i, charsetItem := range charset {\n\t\tcharIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(charsetItem))))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tbytes[i] = charsetItem[charIndex.Int64()]\n\t}\n\n\tfullCharset := strings.Join(charset, \"\")\n\tfor i := len(charset); i < length; i++ {\n\t\tcharIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(fullCharset))))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tbytes[i] = fullCharset[charIndex.Int64()]\n\t}\n\n\tfor i := len(bytes) - 1; i > 0; i-- {\n\t\tj, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tbytes[i], bytes[j.Int64()] = bytes[j.Int64()], bytes[i]\n\t}\n\n\treturn string(bytes), nil\n}\n\nfunc initDatabase(driver, connection string) (*xorm.Engine, error) {\n\tdataConf := &data.Database{Driver: driver, Connection: connection}\n\tif !CheckDBConnection(dataConf) {\n\t\treturn nil, fmt.Errorf(\"database connection check failed\")\n\t}\n\n\tengine, err := data.NewDB(false, dataConf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn engine, nil\n}\n\nfunc printWarning(msg string) {\n\tif runtime.GOOS == \"windows\" {\n\t\tfmt.Printf(\"[WARNING] %s\\n\", msg)\n\t} else {\n\t\tfmt.Printf(\"\\033[31m[WARNING] %s\\033[0m\\n\", msg)\n\t}\n}\n\nfunc confirmAction(prompt string) bool {\n\treader := bufio.NewReader(os.Stdin)\n\tfmt.Printf(\"%s [y/N]: \", prompt)\n\tresponse, err := reader.ReadString('\\n')\n\tif err != nil {\n\t\treturn false\n\t}\n\tresponse = strings.ToLower(strings.TrimSpace(response))\n\treturn response == \"y\" || response == \"yes\"\n}\n"
  },
  {
    "path": "internal/controller/activity_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype ActivityController struct {\n\tactivityService *activity.ActivityService\n}\n\n// NewActivityController new activity controller.\nfunc NewActivityController(\n\tactivityService *activity.ActivityService) *ActivityController {\n\treturn &ActivityController{activityService: activityService}\n}\n\n// GetObjectTimeline get object timeline\n// @Summary get object timeline\n// @Description get object timeline\n// @Tags Comment\n// @Produce json\n// @Param object_id query string false \"object id\"\n// @Param tag_slug_name query string false \"tag slug name\"\n// @Param object_type query string false \"object type\" Enums(question, answer, tag)\n// @Param show_vote query boolean false \"is show vote\"\n// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}\n// @Router /answer/api/v1/activity/timeline [get]\nfunc (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {\n\treq := &schema.GetObjectTimelineReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tif userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil {\n\t\treq.IsAdmin = userInfo.RoleID == role.RoleAdminID\n\t}\n\n\tresp, err := ac.activityService.GetObjectTimeline(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetObjectTimelineDetail get object timeline detail\n// @Summary get object timeline detail\n// @Description get object timeline detail\n// @Tags Comment\n// @Produce json\n// @Param revision_id query string true \"revision id\"\n// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}\n// @Router /answer/api/v1/activity/timeline/detail [get]\nfunc (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) {\n\treq := &schema.GetObjectTimelineDetailReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := ac.activityService.GetObjectTimelineDetail(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/ai_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/schema/mcp_tools\"\n\t\"github.com/apache/answer/internal/service/ai_conversation\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttagcommonser \"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype AIController struct {\n\tsearchService         *content.SearchService\n\tsiteInfoService       siteinfo_common.SiteInfoCommonService\n\ttagCommonService      *tagcommonser.TagCommonService\n\tquestioncommon        *questioncommon.QuestionCommon\n\tcommentRepo           comment.CommentRepo\n\tuserCommon            *usercommon.UserCommon\n\tanswerRepo            answercommon.AnswerRepo\n\tmcpController         *MCPController\n\taiConversationService ai_conversation.AIConversationService\n\tfeatureToggleSvc      *feature_toggle.FeatureToggleService\n}\n\n// NewAIController new site info controller.\nfunc NewAIController(\n\tsearchService *content.SearchService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\ttagCommonService *tagcommonser.TagCommonService,\n\tquestioncommon *questioncommon.QuestionCommon,\n\tcommentRepo comment.CommentRepo,\n\tuserCommon *usercommon.UserCommon,\n\tanswerRepo answercommon.AnswerRepo,\n\tmcpController *MCPController,\n\taiConversationService ai_conversation.AIConversationService,\n\tfeatureToggleSvc *feature_toggle.FeatureToggleService,\n) *AIController {\n\treturn &AIController{\n\t\tsearchService:         searchService,\n\t\tsiteInfoService:       siteInfoService,\n\t\ttagCommonService:      tagCommonService,\n\t\tquestioncommon:        questioncommon,\n\t\tcommentRepo:           commentRepo,\n\t\tuserCommon:            userCommon,\n\t\tanswerRepo:            answerRepo,\n\t\tmcpController:         mcpController,\n\t\taiConversationService: aiConversationService,\n\t\tfeatureToggleSvc:      featureToggleSvc,\n\t}\n}\n\nfunc (c *AIController) ensureAIChatEnabled(ctx *gin.Context) bool {\n\tif c.featureToggleSvc == nil {\n\t\treturn true\n\t}\n\tif err := c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn false\n\t}\n\treturn true\n}\n\ntype ChatCompletionsRequest struct {\n\tMessages       []Message `validate:\"required,gte=1\" json:\"messages\"`\n\tConversationID string    `json:\"conversation_id\"`\n\tUserID         string    `json:\"-\"`\n}\n\ntype Message struct {\n\tRole    string `json:\"role\" binding:\"required\"`\n\tContent string `json:\"content\" binding:\"required\"`\n}\n\ntype ChatCompletionsResponse struct {\n\tID      string   `json:\"id\"`\n\tObject  string   `json:\"object\"`\n\tCreated int64    `json:\"created\"`\n\tModel   string   `json:\"model\"`\n\tChoices []Choice `json:\"choices\"`\n\tUsage   Usage    `json:\"usage\"`\n}\n\ntype StreamResponse struct {\n\tChatCompletionID string         `json:\"chat_completion_id\"`\n\tObject           string         `json:\"object\"`\n\tCreated          int64          `json:\"created\"`\n\tModel            string         `json:\"model\"`\n\tChoices          []StreamChoice `json:\"choices\"`\n}\n\ntype Choice struct {\n\tIndex        int     `json:\"index\"`\n\tMessage      Message `json:\"message\"`\n\tFinishReason string  `json:\"finish_reason\"`\n}\n\ntype StreamChoice struct {\n\tIndex        int     `json:\"index\"`\n\tDelta        Delta   `json:\"delta\"`\n\tFinishReason *string `json:\"finish_reason\"`\n}\n\ntype Delta struct {\n\tRole    string `json:\"role,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n}\n\ntype Usage struct {\n\tPromptTokens     int `json:\"prompt_tokens\"`\n\tCompletionTokens int `json:\"completion_tokens\"`\n\tTotalTokens      int `json:\"total_tokens\"`\n}\n\ntype ConversationContext struct {\n\tConversationID    string\n\tUserID            string\n\tUserQuestion      string\n\tMessages          []*ai_conversation.ConversationMessage\n\tIsNewConversation bool\n\tModel             string\n}\n\nfunc (c *ConversationContext) GetOpenAIMessages() []openai.ChatCompletionMessage {\n\tmessages := make([]openai.ChatCompletionMessage, len(c.Messages))\n\tfor i, msg := range c.Messages {\n\t\tmessages[i] = openai.ChatCompletionMessage{\n\t\t\tRole:    msg.Role,\n\t\t\tContent: msg.Content,\n\t\t}\n\t}\n\treturn messages\n}\n\n// sendStreamData\nfunc sendStreamData(w http.ResponseWriter, data StreamResponse) {\n\tjsonData, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t_, _ = fmt.Fprintf(w, \"data: %s\\n\\n\", string(jsonData))\n\tif f, ok := w.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n}\n\nfunc (c *AIController) ChatCompletions(ctx *gin.Context) {\n\tif !c.ensureAIChatEnabled(ctx) {\n\t\treturn\n\t}\n\taiConfig, err := c.siteInfoService.GetSiteAI(context.Background())\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get AI config: %v\", err)\n\t\thandler.HandleResponse(ctx, errors.BadRequest(\"AI service configuration error\"), nil)\n\t\treturn\n\t}\n\n\tif !aiConfig.Enabled {\n\t\thandler.HandleResponse(ctx, errors.ServiceUnavailable(\"AI service is not enabled\"), nil)\n\t\treturn\n\t}\n\n\taiProvider := aiConfig.GetProvider()\n\n\treq := &ChatCompletionsRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tdata, _ := json.Marshal(req)\n\tlog.Infof(\"ai chat request data: %s\", string(data))\n\n\tctx.Header(\"Content-Type\", \"text/event-stream\")\n\tctx.Header(\"Cache-Control\", \"no-cache\")\n\tctx.Header(\"Connection\", \"keep-alive\")\n\tctx.Header(\"Access-Control-Allow-Origin\", \"*\")\n\tctx.Header(\"Access-Control-Allow-Headers\", \"Cache-Control\")\n\n\tctx.Status(http.StatusOK)\n\n\tw := ctx.Writer\n\n\tif f, ok := w.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n\n\tchatcmplID := \"chatcmpl-\" + token.GenerateToken()\n\tcreated := time.Now().Unix()\n\n\tfirstResponse := StreamResponse{\n\t\tChatCompletionID: chatcmplID,\n\t\tObject:           \"chat.completion.chunk\",\n\t\tCreated:          time.Now().Unix(),\n\t\tModel:            aiProvider.Model,\n\t\tChoices:          []StreamChoice{{Index: 0, Delta: Delta{Role: \"assistant\"}, FinishReason: nil}},\n\t}\n\n\tsendStreamData(w, firstResponse)\n\n\tconversationCtx := c.initializeConversationContext(ctx, aiProvider.Model, req)\n\tif conversationCtx == nil {\n\t\tlog.Error(\"Failed to initialize conversation context\")\n\t\tc.sendErrorResponse(w, chatcmplID, aiProvider.Model, \"Failed to initialize conversation context\")\n\t\treturn\n\t}\n\n\tc.redirectRequestToAI(ctx, w, chatcmplID, conversationCtx)\n\n\tfinishReason := \"stop\"\n\tendResponse := StreamResponse{\n\t\tChatCompletionID: chatcmplID,\n\t\tObject:           \"chat.completion.chunk\",\n\t\tCreated:          created,\n\t\tModel:            aiProvider.Model,\n\t\tChoices:          []StreamChoice{{Index: 0, Delta: Delta{}, FinishReason: &finishReason}},\n\t}\n\n\tsendStreamData(w, endResponse)\n\n\t_, _ = fmt.Fprintf(w, \"data: [DONE]\\n\\n\")\n\tif f, ok := w.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n\n\tc.saveConversationRecord(ctx, chatcmplID, conversationCtx)\n}\n\nfunc (c *AIController) redirectRequestToAI(ctx *gin.Context, w http.ResponseWriter, id string, conversationCtx *ConversationContext) {\n\tclient := c.createOpenAIClient()\n\n\tc.handleAIConversation(ctx, w, id, client, conversationCtx)\n}\n\n// createOpenAIClient\nfunc (c *AIController) createOpenAIClient() *openai.Client {\n\tconfig := openai.DefaultConfig(\"\")\n\tconfig.BaseURL = \"\"\n\n\taiConfig, err := c.siteInfoService.GetSiteAI(context.Background())\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get AI config: %v\", err)\n\t\treturn openai.NewClientWithConfig(config)\n\t}\n\n\tif !aiConfig.Enabled {\n\t\tlog.Warn(\"AI feature is disabled\")\n\t\treturn openai.NewClientWithConfig(config)\n\t}\n\n\taiProvider := aiConfig.GetProvider()\n\n\tconfig = openai.DefaultConfig(aiProvider.APIKey)\n\tconfig.BaseURL = aiProvider.APIHost\n\tif !strings.HasSuffix(config.BaseURL, \"/v1\") {\n\t\tconfig.BaseURL += \"/v1\"\n\t}\n\treturn openai.NewClientWithConfig(config)\n}\n\n// getPromptByLanguage\nfunc (c *AIController) getPromptByLanguage(language i18n.Language, question string) string {\n\taiConfig, err := c.siteInfoService.GetSiteAI(context.Background())\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get AI config: %v\", err)\n\t\treturn c.getDefaultPrompt(language, question)\n\t}\n\n\tvar promptTemplate string\n\n\tswitch language {\n\tcase i18n.LanguageChinese:\n\t\tpromptTemplate = aiConfig.PromptConfig.ZhCN\n\tcase i18n.LanguageEnglish:\n\t\tpromptTemplate = aiConfig.PromptConfig.EnUS\n\tdefault:\n\t\tpromptTemplate = aiConfig.PromptConfig.EnUS\n\t}\n\n\tif promptTemplate == \"\" {\n\t\treturn c.getDefaultPrompt(language, question)\n\t}\n\n\treturn fmt.Sprintf(promptTemplate, question)\n}\n\n// getDefaultPrompt prompt\nfunc (c *AIController) getDefaultPrompt(language i18n.Language, question string) string {\n\tswitch language {\n\tcase i18n.LanguageChinese:\n\t\treturn fmt.Sprintf(constant.DefaultAIPromptConfigZhCN, question)\n\tcase i18n.LanguageEnglish:\n\t\treturn fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question)\n\tdefault:\n\t\treturn fmt.Sprintf(constant.DefaultAIPromptConfigEnUS, question)\n\t}\n}\n\n// initializeConversationContext\nfunc (c *AIController) initializeConversationContext(ctx *gin.Context, model string, req *ChatCompletionsRequest) *ConversationContext {\n\tif len(req.ConversationID) == 0 {\n\t\treq.ConversationID = token.GenerateToken()\n\t}\n\tconversationCtx := &ConversationContext{\n\t\tUserID:         req.UserID,\n\t\tMessages:       make([]*ai_conversation.ConversationMessage, 0),\n\t\tConversationID: req.ConversationID,\n\t\tModel:          model,\n\t}\n\n\tconversationDetail, exist, err := c.aiConversationService.GetConversationDetail(ctx, &schema.AIConversationDetailReq{\n\t\tConversationID: req.ConversationID,\n\t\tUserID:         req.UserID,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to get conversation detail: %v\", err)\n\t\treturn nil\n\t}\n\tif !exist {\n\t\tconversationCtx.UserQuestion = req.Messages[0].Content\n\t\tconversationCtx.Messages = c.buildInitialMessages(ctx, req)\n\t\tconversationCtx.IsNewConversation = true\n\t\treturn conversationCtx\n\t}\n\tconversationCtx.IsNewConversation = false\n\n\tfor _, record := range conversationDetail.Records {\n\t\tconversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{\n\t\t\tChatCompletionID: record.ChatCompletionID,\n\t\t\tRole:             record.Role,\n\t\t\tContent:          record.Content,\n\t\t})\n\t}\n\tconversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{\n\t\tRole:    req.Messages[0].Role,\n\t\tContent: req.Messages[0].Content,\n\t})\n\treturn conversationCtx\n}\n\n// buildInitialMessages\nfunc (c *AIController) buildInitialMessages(ctx *gin.Context, req *ChatCompletionsRequest) []*ai_conversation.ConversationMessage {\n\tquestion := \"\"\n\tif len(req.Messages) == 1 {\n\t\tquestion = req.Messages[0].Content\n\t} else {\n\t\tmessages := make([]*ai_conversation.ConversationMessage, len(req.Messages))\n\t\tfor i, msg := range req.Messages {\n\t\t\tmessages[i] = &ai_conversation.ConversationMessage{\n\t\t\t\tRole:    msg.Role,\n\t\t\t\tContent: msg.Content,\n\t\t\t}\n\t\t}\n\t\treturn messages\n\t}\n\n\tcurrentLang := handler.GetLangByCtx(ctx)\n\n\tprompt := c.getPromptByLanguage(currentLang, question)\n\n\treturn []*ai_conversation.ConversationMessage{{Role: openai.ChatMessageRoleUser, Content: prompt}}\n}\n\n// saveConversationRecord\nfunc (c *AIController) saveConversationRecord(ctx context.Context, chatcmplID string, conversationCtx *ConversationContext) {\n\tif conversationCtx == nil || len(conversationCtx.Messages) == 0 {\n\t\treturn\n\t}\n\n\tif conversationCtx.IsNewConversation {\n\t\ttopic := conversationCtx.UserQuestion\n\t\tif topic == \"\" {\n\t\t\tlog.Warn(\"No user message found for new conversation\")\n\t\t\treturn\n\t\t}\n\n\t\terr := c.aiConversationService.CreateConversation(ctx, conversationCtx.UserID, conversationCtx.ConversationID, topic)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"Failed to create conversation: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\terr := c.aiConversationService.SaveConversationRecords(ctx, conversationCtx.ConversationID, chatcmplID, conversationCtx.Messages)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to save conversation records: %v\", err)\n\t}\n}\n\nfunc (c *AIController) handleAIConversation(ctx *gin.Context, w http.ResponseWriter, id string, client *openai.Client, conversationCtx *ConversationContext) {\n\tmaxRounds := 10\n\tmessages := conversationCtx.GetOpenAIMessages()\n\n\tfor round := range maxRounds {\n\t\tlog.Debugf(\"AI conversation round: %d\", round+1)\n\n\t\taiReq := openai.ChatCompletionRequest{\n\t\t\tModel:    conversationCtx.Model,\n\t\t\tMessages: messages,\n\t\t\tTools:    c.getMCPTools(),\n\t\t\tStream:   true,\n\t\t}\n\n\t\ttoolCalls, newMessages, finished, aiResponse := c.processAIStream(ctx, w, id, conversationCtx.Model, client, aiReq, messages)\n\t\tmessages = newMessages\n\n\t\tif aiResponse != \"\" {\n\t\t\tconversationCtx.Messages = append(conversationCtx.Messages, &ai_conversation.ConversationMessage{\n\t\t\t\tRole:    \"assistant\",\n\t\t\t\tContent: aiResponse,\n\t\t\t})\n\t\t}\n\n\t\tif finished {\n\t\t\treturn\n\t\t}\n\n\t\tif len(toolCalls) > 0 {\n\t\t\tmessages = c.executeToolCalls(ctx, w, id, conversationCtx.Model, toolCalls, messages)\n\t\t} else {\n\t\t\treturn\n\t\t}\n\t}\n\n\tlog.Warnf(\"AI conversation reached maximum rounds limit: %d\", maxRounds)\n}\n\n// processAIStream\nfunc (c *AIController) processAIStream(\n\t_ *gin.Context, w http.ResponseWriter, id, model string, client *openai.Client, aiReq openai.ChatCompletionRequest, messages []openai.ChatCompletionMessage) (\n\t[]openai.ToolCall, []openai.ChatCompletionMessage, bool, string) {\n\tstream, err := client.CreateChatCompletionStream(context.Background(), aiReq)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to create stream: %v\", err)\n\t\tc.sendErrorResponse(w, id, model, \"Failed to create AI stream\")\n\t\treturn nil, messages, true, \"\"\n\t}\n\tdefer func() {\n\t\t_ = stream.Close()\n\t}()\n\n\tvar currentToolCalls []openai.ToolCall\n\tvar accumulatedContent strings.Builder\n\tvar accumulatedMessage openai.ChatCompletionMessage\n\ttoolCallsMap := make(map[int]*openai.ToolCall)\n\n\tfor {\n\t\tresponse, err := stream.Recv()\n\t\tif err != nil {\n\t\t\tif err.Error() == \"EOF\" {\n\t\t\t\tlog.Info(\"Stream finished\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlog.Errorf(\"Stream error: %v\", err)\n\t\t\tbreak\n\t\t}\n\n\t\tchoice := response.Choices[0]\n\n\t\tif len(choice.Delta.ToolCalls) > 0 {\n\t\t\tfor _, deltaToolCall := range choice.Delta.ToolCalls {\n\t\t\t\tindex := *deltaToolCall.Index\n\n\t\t\t\tif _, exists := toolCallsMap[index]; !exists {\n\t\t\t\t\ttoolCallsMap[index] = &openai.ToolCall{\n\t\t\t\t\t\tID:   deltaToolCall.ID,\n\t\t\t\t\t\tType: deltaToolCall.Type,\n\t\t\t\t\t\tFunction: openai.FunctionCall{\n\t\t\t\t\t\t\tName:      deltaToolCall.Function.Name,\n\t\t\t\t\t\t\tArguments: deltaToolCall.Function.Arguments,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif deltaToolCall.Function.Arguments != \"\" {\n\t\t\t\t\t\ttoolCallsMap[index].Function.Arguments += deltaToolCall.Function.Arguments\n\t\t\t\t\t}\n\t\t\t\t\tif deltaToolCall.Function.Name != \"\" {\n\t\t\t\t\t\ttoolCallsMap[index].Function.Name = deltaToolCall.Function.Name\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif choice.Delta.Content != \"\" {\n\t\t\taccumulatedContent.WriteString(choice.Delta.Content)\n\n\t\t\tcontentResponse := StreamResponse{\n\t\t\t\tChatCompletionID: id,\n\t\t\t\tObject:           \"chat.completion.chunk\",\n\t\t\t\tCreated:          time.Now().Unix(),\n\t\t\t\tModel:            model,\n\t\t\t\tChoices: []StreamChoice{\n\t\t\t\t\t{\n\t\t\t\t\t\tIndex: 0,\n\t\t\t\t\t\tDelta: Delta{\n\t\t\t\t\t\t\tContent: choice.Delta.Content,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFinishReason: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tsendStreamData(w, contentResponse)\n\t\t}\n\n\t\tif len(choice.FinishReason) > 0 {\n\t\t\tif choice.FinishReason == \"tool_calls\" {\n\t\t\t\tfor _, toolCall := range toolCallsMap {\n\t\t\t\t\tcurrentToolCalls = append(currentToolCalls, *toolCall)\n\t\t\t\t}\n\t\t\t\treturn currentToolCalls, messages, false, accumulatedContent.String()\n\t\t\t} else {\n\t\t\t\taiResponseContent := accumulatedContent.String()\n\t\t\t\tif aiResponseContent != \"\" {\n\t\t\t\t\taccumulatedMessage = openai.ChatCompletionMessage{\n\t\t\t\t\t\tRole:    openai.ChatMessageRoleAssistant,\n\t\t\t\t\t\tContent: aiResponseContent,\n\t\t\t\t\t}\n\t\t\t\t\tmessages = append(messages, accumulatedMessage)\n\t\t\t\t}\n\t\t\t\treturn nil, messages, true, aiResponseContent\n\t\t\t}\n\t\t}\n\t}\n\n\taiResponseContent := accumulatedContent.String()\n\tif aiResponseContent != \"\" {\n\t\taccumulatedMessage = openai.ChatCompletionMessage{\n\t\t\tRole:    openai.ChatMessageRoleAssistant,\n\t\t\tContent: aiResponseContent,\n\t\t}\n\t\tmessages = append(messages, accumulatedMessage)\n\t}\n\n\tif len(toolCallsMap) > 0 {\n\t\tfor _, toolCall := range toolCallsMap {\n\t\t\tcurrentToolCalls = append(currentToolCalls, *toolCall)\n\t\t}\n\t\treturn currentToolCalls, messages, false, aiResponseContent\n\t}\n\n\treturn currentToolCalls, messages, len(currentToolCalls) == 0, aiResponseContent\n}\n\n// executeToolCalls\nfunc (c *AIController) executeToolCalls(ctx *gin.Context, _ http.ResponseWriter, _, _ string, toolCalls []openai.ToolCall, messages []openai.ChatCompletionMessage) []openai.ChatCompletionMessage {\n\tvalidToolCalls := make([]openai.ToolCall, 0)\n\tfor _, toolCall := range toolCalls {\n\t\tif toolCall.ID == \"\" || toolCall.Function.Name == \"\" {\n\t\t\tlog.Errorf(\"Invalid tool call: missing required fields. ID: %s, Function: %v\", toolCall.ID, toolCall.Function)\n\t\t\tcontinue\n\t\t}\n\n\t\tif toolCall.Function.Arguments == \"\" {\n\t\t\ttoolCall.Function.Arguments = \"{}\"\n\t\t}\n\n\t\tvalidToolCalls = append(validToolCalls, toolCall)\n\t\tlog.Debugf(\"Valid tool call: ID=%s, Name=%s, Arguments=%s\", toolCall.ID, toolCall.Function.Name, toolCall.Function.Arguments)\n\t}\n\n\tif len(validToolCalls) == 0 {\n\t\tlog.Warn(\"No valid tool calls found\")\n\t\treturn messages\n\t}\n\n\tassistantMsg := openai.ChatCompletionMessage{\n\t\tRole:      openai.ChatMessageRoleAssistant,\n\t\tToolCalls: validToolCalls,\n\t}\n\tmessages = append(messages, assistantMsg)\n\n\tfor _, toolCall := range validToolCalls {\n\t\tif toolCall.Function.Name != \"\" {\n\t\t\tvar args map[string]any\n\t\t\tif err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {\n\t\t\t\tlog.Errorf(\"Failed to parse tool arguments for %s: %v, arguments: %s\", toolCall.Function.Name, err, toolCall.Function.Arguments)\n\t\t\t\terrorResult := fmt.Sprintf(\"Error parsing tool arguments: %v\", err)\n\t\t\t\ttoolMessage := openai.ChatCompletionMessage{\n\t\t\t\t\tRole:       openai.ChatMessageRoleTool,\n\t\t\t\t\tContent:    errorResult,\n\t\t\t\t\tToolCallID: toolCall.ID,\n\t\t\t\t}\n\t\t\t\tmessages = append(messages, toolMessage)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tresult, err := c.callMCPTool(ctx, toolCall.Function.Name, args)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"Failed to call MCP tool %s: %v\", toolCall.Function.Name, err)\n\t\t\t\tresult = fmt.Sprintf(\"Error calling tool %s: %v\", toolCall.Function.Name, err)\n\t\t\t}\n\n\t\t\ttoolMessage := openai.ChatCompletionMessage{\n\t\t\t\tRole:       openai.ChatMessageRoleTool,\n\t\t\t\tContent:    result,\n\t\t\t\tToolCallID: toolCall.ID,\n\t\t\t}\n\t\t\tmessages = append(messages, toolMessage)\n\t\t}\n\t}\n\n\treturn messages\n}\n\n// sendErrorResponse send error response in stream\nfunc (c *AIController) sendErrorResponse(w http.ResponseWriter, id, model, errorMsg string) {\n\terrorResponse := StreamResponse{\n\t\tChatCompletionID: id,\n\t\tObject:           \"chat.completion.chunk\",\n\t\tCreated:          time.Now().Unix(),\n\t\tModel:            model,\n\t\tChoices: []StreamChoice{\n\t\t\t{\n\t\t\t\tIndex: 0,\n\t\t\t\tDelta: Delta{\n\t\t\t\t\tContent: fmt.Sprintf(\"Error: %s\", errorMsg),\n\t\t\t\t},\n\t\t\t\tFinishReason: nil,\n\t\t\t},\n\t\t},\n\t}\n\tsendStreamData(w, errorResponse)\n}\n\n// getMCPTools\nfunc (c *AIController) getMCPTools() []openai.Tool {\n\topenaiTools := make([]openai.Tool, 0)\n\tfor _, mcpTool := range mcp_tools.MCPToolsList {\n\t\topenaiTool := c.convertMCPToolToOpenAI(mcpTool)\n\t\topenaiTools = append(openaiTools, openaiTool)\n\t}\n\n\treturn openaiTools\n}\n\n// convertMCPToolToOpenAI\nfunc (c *AIController) convertMCPToolToOpenAI(mcpTool mcp.Tool) openai.Tool {\n\tproperties := make(map[string]any)\n\trequired := make([]string, 0)\n\n\tmaps.Copy(properties, mcpTool.InputSchema.Properties)\n\n\trequired = append(required, mcpTool.InputSchema.Required...)\n\n\tparameters := map[string]any{\n\t\t\"type\":       \"object\",\n\t\t\"properties\": properties,\n\t}\n\n\tif len(required) > 0 {\n\t\tparameters[\"required\"] = required\n\t}\n\n\treturn openai.Tool{\n\t\tType: openai.ToolTypeFunction,\n\t\tFunction: &openai.FunctionDefinition{\n\t\t\tName:        mcpTool.Name,\n\t\t\tDescription: mcpTool.Description,\n\t\t\tParameters:  parameters,\n\t\t},\n\t}\n}\n\n// callMCPTool\nfunc (c *AIController) callMCPTool(ctx context.Context, toolName string, arguments map[string]any) (string, error) {\n\trequest := mcp.CallToolRequest{\n\t\tRequest: mcp.Request{},\n\t\tParams: struct {\n\t\t\tName      string    `json:\"name\"`\n\t\t\tArguments any       `json:\"arguments,omitempty\"`\n\t\t\tMeta      *mcp.Meta `json:\"_meta,omitempty\"`\n\t\t}{\n\t\t\tName:      toolName,\n\t\t\tArguments: arguments,\n\t\t},\n\t}\n\n\tvar result *mcp.CallToolResult\n\tvar err error\n\n\tlog.Debugf(\"Calling MCP tool: %s with arguments: %v\", toolName, arguments)\n\n\tswitch toolName {\n\tcase \"get_questions\":\n\t\tresult, err = c.mcpController.MCPQuestionsHandler()(ctx, request)\n\tcase \"get_answers_by_question_id\":\n\t\tresult, err = c.mcpController.MCPAnswersHandler()(ctx, request)\n\tcase \"get_comments\":\n\t\tresult, err = c.mcpController.MCPCommentsHandler()(ctx, request)\n\tcase \"get_tags\":\n\t\tresult, err = c.mcpController.MCPTagsHandler()(ctx, request)\n\tcase \"get_tag_detail\":\n\t\tresult, err = c.mcpController.MCPTagDetailsHandler()(ctx, request)\n\tcase \"get_user\":\n\t\tresult, err = c.mcpController.MCPUserDetailsHandler()(ctx, request)\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unknown tool: %s\", toolName)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdata, _ := json.Marshal(result)\n\tlog.Debugf(\"MCP tool %s called successfully, result: %v\", toolName, string(data))\n\n\tif result != nil && len(result.Content) > 0 {\n\t\tif textContent, ok := result.Content[0].(mcp.TextContent); ok {\n\t\t\treturn textContent.Text, nil\n\t\t}\n\t}\n\n\treturn \"No result found\", nil\n}\n"
  },
  {
    "path": "internal/controller/ai_conversation_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/ai_conversation\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// AIConversationController ai conversation controller\ntype AIConversationController struct {\n\taiConversationService ai_conversation.AIConversationService\n\tfeatureToggleSvc      *feature_toggle.FeatureToggleService\n}\n\n// NewAIConversationController creates a new AI conversation controller\nfunc NewAIConversationController(\n\taiConversationService ai_conversation.AIConversationService,\n\tfeatureToggleSvc *feature_toggle.FeatureToggleService,\n) *AIConversationController {\n\treturn &AIConversationController{\n\t\taiConversationService: aiConversationService,\n\t\tfeatureToggleSvc:      featureToggleSvc,\n\t}\n}\n\nfunc (ctrl *AIConversationController) ensureEnabled(ctx *gin.Context) bool {\n\tif ctrl.featureToggleSvc == nil {\n\t\treturn true\n\t}\n\tif err := ctrl.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// GetConversationList gets conversation list\n// @Summary get conversation list\n// @Description get conversation list\n// @Tags ai-conversation\n// @Accept json\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.AIConversationListItem}}\n// @Router /answer/api/v1/ai/conversation/page [get]\nfunc (ctrl *AIConversationController) GetConversationList(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := ctrl.aiConversationService.GetConversationList(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetConversationDetail gets conversation detail\n// @Summary get conversation detail\n// @Description get conversation detail\n// @Tags ai-conversation\n// @Accept json\n// @Produce json\n// @Param conversation_id query string true \"conversation id\"\n// @Success 200 {object} handler.RespBody{data=schema.AIConversationDetailResp}\n// @Router /answer/api/v1/ai/conversation [get]\nfunc (ctrl *AIConversationController) GetConversationDetail(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationDetailReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, _, err := ctrl.aiConversationService.GetConversationDetail(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// VoteRecord vote record\n// @Summary vote record\n// @Description vote record\n// @Tags ai-conversation\n// @Accept json\n// @Produce json\n// @Param data body schema.AIConversationVoteReq true \"vote request\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/ai/conversation/vote [post]\nfunc (ctrl *AIConversationController) VoteRecord(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationVoteReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := ctrl.aiConversationService.VoteRecord(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/answer_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// AnswerController answer controller\ntype AnswerController struct {\n\tanswerService         *content.AnswerService\n\trankService           *rank.RankService\n\tactionService         *action.CaptchaService\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n\trateLimitMiddleware   *middleware.RateLimitMiddleware\n}\n\n// NewAnswerController new controller\nfunc NewAnswerController(\n\tanswerService *content.AnswerService,\n\trankService *rank.RankService,\n\tactionService *action.CaptchaService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n\trateLimitMiddleware *middleware.RateLimitMiddleware,\n) *AnswerController {\n\treturn &AnswerController{\n\t\tanswerService:         answerService,\n\t\trankService:           rankService,\n\t\tactionService:         actionService,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t\trateLimitMiddleware:   rateLimitMiddleware,\n\t}\n}\n\n// RemoveAnswer delete answer\n// @Summary delete answer\n// @Description delete answer\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.RemoveAnswerReq true \"answer\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/answer [delete]\nfunc (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {\n\treq := &schema.RemoveAnswerReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tobjectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)\n\tcanList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanDelete = canList[0] || objectOwner\n\tif !req.CanDelete {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = ac.answerService.RemoveAnswer(ctx, req)\n\tif !isAdmin {\n\t\tac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// RecoverAnswer recover answer\n// @Summary recover answer\n// @Description recover the deleted answer\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.RecoverAnswerReq true \"answer\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/answer/recover [post]\nfunc (ac *AnswerController) RecoverAnswer(ctx *gin.Context) {\n\treq := &schema.RecoverAnswerReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.AnswerID = uid.DeShortID(req.AnswerID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !canList[0] {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = ac.answerService.RecoverAnswer(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetAnswerInfo get answer info\n// @Summary Get Answer Detail\n// @Description Get Answer Detail\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Param id query string true \"id\"\n// @Success 200 {object} handler.RespBody{data=schema.GetAnswerInfoResp}\n// @Router /answer/api/v1/answer/info [get]\nfunc (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) {\n\tid := ctx.Query(\"id\")\n\tid = uid.DeShortID(id)\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\n\tinfo, questionInfo, has, err := ac.answerService.Get(ctx, id, userID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, gin.H{})\n\t\treturn\n\t}\n\tif !has {\n\t\thandler.HandleResponse(ctx, fmt.Errorf(\"\"), gin.H{})\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, err, &schema.GetAnswerInfoResp{\n\t\tInfo:     info,\n\t\tQuestion: questionInfo,\n\t})\n}\n\n// AddAnswer add answer\n// @Summary Add Answer\n// @Description add answer\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AnswerAddReq true \"add answer request\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/answer [post]\nfunc (ac *AnswerController) AddAnswer(ctx *gin.Context) {\n\treq := &schema.AnswerAddReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treject, rejectKey := ac.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)\n\tif reject {\n\t\treturn\n\t}\n\tdefer func() {\n\t\t// If status is not 200 means that the bad request has been returned, so the record should be cleared\n\t\tif ctx.Writer.Status() != http.StatusOK {\n\t\t\tac.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)\n\t\t}\n\t}()\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerEdit,\n\t\tpermission.AnswerDelete,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tlinkUrlLimitUser := canList[2]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionAnswer, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tcan, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\twrite, err := ac.siteInfoCommonService.GetSiteQuestion(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif write.RestrictAnswer {\n\t\t// check if there's already an answer by this user\n\t\tids, err := ac.answerService.GetCountByUserIDQuestionID(ctx, req.UserID, req.QuestionID)\n\t\tif err != nil {\n\t\t\thandler.HandleResponse(ctx, err, nil)\n\t\t\treturn\n\t\t}\n\t\tif len(ids) >= 1 {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.AnswerRestrictAnswer), nil)\n\t\t\treturn\n\t\t}\n\t}\n\n\treq.UserAgent = ctx.GetHeader(\"User-Agent\")\n\treq.IP = ctx.ClientIP()\n\n\tanswerID, err := ac.answerService.Insert(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID)\n\t}\n\tinfo, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !has {\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\n\tobjectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID)\n\treq.CanEdit = canList[0] || objectOwner\n\treq.CanDelete = canList[1] || objectOwner\n\tinfo.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, info.UserID,\n\t\t0, req.CanEdit, req.CanDelete, false)\n\thandler.HandleResponse(ctx, nil, gin.H{\n\t\t\"info\":     info,\n\t\t\"question\": questionInfo,\n\t})\n}\n\n// UpdateAnswer update answer\n// @Summary Update Answer\n// @Description Update Answer\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AnswerUpdateReq true \"AnswerUpdateReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/answer [put]\nfunc (ac *AnswerController) UpdateAnswer(ctx *gin.Context) {\n\treq := &schema.AnswerUpdateReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerEdit,\n\t\tpermission.AnswerEditWithoutReview,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tlinkUrlLimitUser := canList[2]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tobjectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)\n\treq.CanEdit = canList[0] || objectOwner\n\treq.NoNeedReview = canList[1] || objectOwner\n\tif !req.CanEdit {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\t_, err = ac.answerService.Update(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)\n\t}\n\t_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview})\n}\n\n// AnswerList godoc\n// @Summary AnswerList\n// @Description AnswerList <br> <b>order</b> (default or updated)\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Param question_id query string true \"question_id\"\n// @Param order query string true \"order\"\n// @Param page query string true \"page\"\n// @Param page_size query string true \"page_size\"\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/answer/page [get]\nfunc (ac *AnswerController) AnswerList(ctx *gin.Context) {\n\treq := &schema.AnswerListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\n\tcanList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerEdit,\n\t\tpermission.AnswerDelete,\n\t\tpermission.AnswerUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = canList[0]\n\treq.CanDelete = canList[1]\n\treq.CanRecover = canList[2]\n\n\tlist, count, err := ac.answerService.SearchList(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, gin.H{\n\t\t\"list\":  list,\n\t\t\"count\": count,\n\t})\n}\n\n// AcceptAnswer accept answer\n// @Summary Accept Answer\n// @Description Accept Answer\n// @Tags Answer\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AcceptAnswerReq true \"AcceptAnswerReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/answer/acceptance [post]\nfunc (ac *AnswerController) AcceptAnswer(ctx *gin.Context) {\n\treq := &schema.AcceptAnswerReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.AnswerID = uid.DeShortID(req.AnswerID)\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\tcan, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = ac.answerService.AcceptAnswer(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// AdminUpdateAnswerStatus update answer status\n// @Summary update answer status\n// @Description update answer status\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AdminUpdateAnswerStatusReq true \"AdminUpdateAnswerStatusReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/answer/status [put]\nfunc (ac *AnswerController) AdminUpdateAnswerStatus(ctx *gin.Context) {\n\treq := &schema.AdminUpdateAnswerStatusReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.AnswerID = uid.DeShortID(req.AnswerID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := ac.answerService.AdminSetAnswerStatus(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/badge_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype BadgeController struct {\n\tbadgeService      *badge.BadgeService\n\tbadgeAwardService *badge.BadgeAwardService\n}\n\nfunc NewBadgeController(\n\tbadgeService *badge.BadgeService,\n\tbadgeAwardService *badge.BadgeAwardService) *BadgeController {\n\treturn &BadgeController{\n\t\tbadgeService:      badgeService,\n\t\tbadgeAwardService: badgeAwardService,\n\t}\n}\n\n// GetBadgeList list all badges\n// @Summary list all badges group by group\n// @Description list all badges group by group\n// @Tags api-badge\n// @Accept json\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListResp}\n// @Router /answer/api/v1/badges [get]\nfunc (b *BadgeController) GetBadgeList(ctx *gin.Context) {\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := b.badgeService.ListByGroup(ctx, userID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetBadgeInfo get badge info\n// @Summary get badge info\n// @Description get badge info\n// @Tags api-badge\n// @Accept json\n// @Produce json\n// @Param id query string true \"id\" default(string)\n// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp}\n// @Router /answer/api/v1/badge [get]\nfunc (b *BadgeController) GetBadgeInfo(ctx *gin.Context) {\n\tid := ctx.Query(\"id\")\n\tid = uid.DeShortID(id)\n\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := b.badgeService.GetBadgeInfo(ctx, id, userID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetBadgeAwardList get badge award list\n// @Summary get badge award list\n// @Description get badge award list\n// @Tags api-badge\n// @Accept json\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Param badge_id query string true \"badge id\"\n// @Param username query string false \"only list the award by username\"\n// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp}\n// @Router /answer/api/v1/badge/awards/page [get]\nfunc (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) {\n\treq := &schema.GetBadgeAwardWithPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.BadgeID = uid.DeShortID(req.BadgeID)\n\n\tresp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))\n}\n\n// GetAllBadgeAwardListByUsername get user badge award list\n// @Summary get user badge award list\n// @Description get user badge award list\n// @Tags api-badge\n// @Accept json\n// @Produce json\n// @Param username query string true \"user name\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp}\n// @Router /answer/api/v1/badge/user/awards [get]\nfunc (b *BadgeController) GetAllBadgeAwardListByUsername(ctx *gin.Context) {\n\treq := &schema.GetUserBadgeAwardListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, total, err := b.badgeAwardService.GetUserBadgeAwardList(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))\n}\n\n// GetRecentBadgeAwardListByUsername get user badge award list\n// @Summary get user badge award list\n// @Description get user badge award list\n// @Tags api-badge\n// @Accept json\n// @Produce json\n// @Param username query string true \"user name\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp}\n// @Router /answer/api/v1/badge/user/awards/recent [get]\nfunc (b *BadgeController) GetRecentBadgeAwardListByUsername(ctx *gin.Context) {\n\treq := &schema.GetUserBadgeAwardListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.Limit = 10\n\n\tresp, total, err := b.badgeAwardService.GetUserRecentBadgeAwardList(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))\n}\n"
  },
  {
    "path": "internal/controller/collection_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/collection\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// CollectionController collection controller\ntype CollectionController struct {\n\tcollectionService *collection.CollectionService\n}\n\n// NewCollectionController new controller\nfunc NewCollectionController(collectionService *collection.CollectionService) *CollectionController {\n\treturn &CollectionController{collectionService: collectionService}\n}\n\n// CollectionSwitch add collection\n// @Summary add collection\n// @Description add collection\n// @Tags Collection\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.CollectionSwitchReq true \"collection\"\n// @Success 200 {object} handler.RespBody{data=schema.CollectionSwitchResp}\n// @Router /answer/api/v1/collection/switch [post]\nfunc (cc *CollectionController) CollectionSwitch(ctx *gin.Context) {\n\treq := &schema.CollectionSwitchReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := cc.collectionService.CollectionSwitch(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/comment_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// CommentController comment controller\ntype CommentController struct {\n\tcommentService      *comment.CommentService\n\trankService         *rank.RankService\n\tactionService       *action.CaptchaService\n\trateLimitMiddleware *middleware.RateLimitMiddleware\n}\n\n// NewCommentController new controller\nfunc NewCommentController(\n\tcommentService *comment.CommentService,\n\trankService *rank.RankService,\n\tactionService *action.CaptchaService,\n\trateLimitMiddleware *middleware.RateLimitMiddleware,\n) *CommentController {\n\treturn &CommentController{\n\t\tcommentService:      commentService,\n\t\trankService:         rankService,\n\t\tactionService:       actionService,\n\t\trateLimitMiddleware: rateLimitMiddleware,\n\t}\n}\n\n// AddComment add comment\n// @Summary add comment\n// @Description add comment\n// @Tags Comment\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AddCommentReq true \"comment\"\n// @Success 200 {object} handler.RespBody{data=schema.GetCommentResp}\n// @Router /answer/api/v1/comment [post]\nfunc (cc *CommentController) AddComment(ctx *gin.Context) {\n\treq := &schema.AddCommentReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treject, rejectKey := cc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)\n\tif reject {\n\t\treturn\n\t}\n\tdefer func() {\n\t\t// If status is not 200 means that the bad request has been returned, so the record should be cleared\n\t\tif ctx.Writer.Status() != http.StatusOK {\n\t\t\tcc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)\n\t\t}\n\t}()\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.CommentAdd,\n\t\tpermission.CommentEdit,\n\t\tpermission.CommentDelete,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tlinkUrlLimitUser := canList[3]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionComment, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\treq.CanAdd = canList[0]\n\treq.CanEdit = canList[1]\n\treq.CanDelete = canList[2]\n\tif !req.CanAdd {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\treq.UserAgent = ctx.GetHeader(\"User-Agent\")\n\treq.IP = ctx.ClientIP()\n\n\tresp, err := cc.commentService.AddComment(ctx, req)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// RemoveComment remove comment\n// @Summary remove comment\n// @Description remove comment\n// @Tags Comment\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.RemoveCommentReq true \"comment\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/comment [delete]\nfunc (cc *CommentController) RemoveComment(ctx *gin.Context) {\n\treq := &schema.RemoveCommentReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\tcan, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = cc.commentService.RemoveComment(ctx, req)\n\tif !isAdmin {\n\t\tcc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateComment update comment\n// @Summary update comment\n// @Description update comment\n// @Tags Comment\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateCommentReq true \"comment\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/comment [put]\nfunc (cc *CommentController) UpdateComment(ctx *gin.Context) {\n\treq := &schema.UpdateCommentReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetIsAdminFromContext(ctx)\n\tcanList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.CommentEdit,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = canList[0] || cc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.CommentID)\n\tlinkUrlLimitUser := canList[1]\n\tif !req.CanEdit {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\tif !req.IsAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresp, err := cc.commentService.UpdateComment(ctx, req)\n\tif !req.IsAdmin || !linkUrlLimitUser {\n\t\tcc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetCommentWithPage get comment page\n// @Summary get comment page\n// @Description get comment page\n// @Tags Comment\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Param object_id query string true \"object id\"\n// @Param query_cond query string false \"query condition\" Enums(vote)\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}\n// @Router /answer/api/v1/comment/page [get]\nfunc (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {\n\treq := &schema.GetCommentWithPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.CommentID = uid.DeShortID(req.CommentID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.CommentEdit,\n\t\tpermission.CommentDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = canList[0]\n\treq.CanDelete = canList[1]\n\n\tresp, err := cc.commentService.GetCommentWithPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetCommentPersonalWithPage user personal comment list\n// @Summary user personal comment list\n// @Description user personal comment list\n// @Tags Comment\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Param username query string false \"username\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentPersonalWithPageResp}}\n// @Router /answer/api/v1/personal/comment/page [get]\nfunc (cc *CommentController) GetCommentPersonalWithPage(ctx *gin.Context) {\n\treq := &schema.GetCommentPersonalWithPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := cc.commentService.GetCommentPersonalWithPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetComment godoc\n// @Summary get comment by id\n// @Description get comment by id\n// @Tags Comment\n// @Produce json\n// @Param id query string true \"id\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}\n// @Router /answer/api/v1/comment [get]\nfunc (cc *CommentController) GetComment(ctx *gin.Context) {\n\treq := &schema.GetCommentReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.CommentEdit,\n\t\tpermission.CommentDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = canList[0]\n\treq.CanDelete = canList[1]\n\n\tresp, err := cc.commentService.GetComment(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/connector_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nconst (\n\tcommonRouterPrefix            = \"/answer/api/v1\"\n\tConnectorLoginRouterPrefix    = \"/connector/login/\"\n\tConnectorRedirectRouterPrefix = \"/connector/redirect/\"\n)\n\n// ConnectorController comment controller\ntype ConnectorController struct {\n\tsiteInfoService     siteinfo_common.SiteInfoCommonService\n\tuserExternalService *user_external_login.UserExternalLoginService\n\temailService        *export.EmailService\n}\n\n// NewConnectorController new controller\nfunc NewConnectorController(\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\temailService *export.EmailService,\n\tuserExternalService *user_external_login.UserExternalLoginService,\n) *ConnectorController {\n\treturn &ConnectorController{\n\t\tsiteInfoService:     siteInfoService,\n\t\tuserExternalService: userExternalService,\n\t\temailService:        emailService,\n\t}\n}\n\n// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name\n// We can't register specific router for each connector when application start, because the plugin status will be changed by admin.\n// If the plugin is disabled, the router should be unavailable.\nfunc (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) {\n\tslugName := ctx.Param(\"name\")\n\tvar c plugin.Connector\n\t_ = plugin.CallConnector(func(connector plugin.Connector) error {\n\t\tif connector.ConnectorSlugName() == slugName {\n\t\t\tc = connector\n\t\t}\n\t\treturn nil\n\t})\n\tif c == nil {\n\t\tlog.Errorf(\"connector %s not found\", slugName)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\tcc.ConnectorLogin(c)(ctx)\n}\n\nfunc (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) {\n\tslugName := ctx.Param(\"name\")\n\tvar c plugin.Connector\n\t_ = plugin.CallConnector(func(connector plugin.Connector) error {\n\t\tif connector.ConnectorSlugName() == slugName {\n\t\t\tc = connector\n\t\t}\n\t\treturn nil\n\t})\n\tif c == nil {\n\t\tlog.Errorf(\"connector %s not found\", slugName)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\tcc.ConnectorRedirect(c)(ctx)\n}\n\nfunc (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) {\n\treturn func(ctx *gin.Context) {\n\t\tgeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\t\treturn\n\t\t}\n\n\t\treceiverURL := fmt.Sprintf(\"%s%s%s%s\", general.SiteUrl,\n\t\t\tcommonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())\n\t\tredirectURL := connector.ConnectorSender(ctx, receiverURL)\n\t\tif len(redirectURL) > 0 {\n\t\t\tctx.Redirect(http.StatusFound, redirectURL)\n\t\t}\n\t}\n}\n\nfunc (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {\n\treturn func(ctx *gin.Context) {\n\t\tsiteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site info failed: %v\", err)\n\t\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\t\treturn\n\t\t}\n\t\treceiverURL := fmt.Sprintf(\"%s%s%s%s\", siteGeneral.SiteUrl,\n\t\t\tcommonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())\n\t\tuserInfo, err := connector.ConnectorReceiver(ctx, receiverURL)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"connector received failed, error info: %v, response data is: %s\", err, userInfo.MetaInfo)\n\t\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"connector received: %+v\", userInfo)\n\t\tu := &schema.ExternalLoginUserInfoCache{\n\t\t\tProvider:    connector.ConnectorSlugName(),\n\t\t\tExternalID:  userInfo.ExternalID,\n\t\t\tDisplayName: userInfo.DisplayName,\n\t\t\tUsername:    userInfo.Username,\n\t\t\tEmail:       userInfo.Email,\n\t\t\tAvatar:      userInfo.Avatar,\n\t\t\tMetaInfo:    userInfo.MetaInfo,\n\t\t}\n\t\tresp, err := cc.userExternalService.ExternalLogin(ctx, u)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"external login failed: %v\", err)\n\t\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\t\treturn\n\t\t}\n\t\tif len(resp.ErrMsg) > 0 {\n\t\t\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"/50x?title=%s&msg=%s\", resp.ErrTitle, resp.ErrMsg))\n\t\t\treturn\n\t\t}\n\t\tif len(resp.AccessToken) > 0 {\n\t\t\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"%s/users/auth-landing?access_token=%s\",\n\t\t\t\tsiteGeneral.SiteUrl, resp.AccessToken))\n\t\t} else {\n\t\t\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"%s/users/confirm-email?binding_key=%s\",\n\t\t\t\tsiteGeneral.SiteUrl, resp.BindingKey))\n\t\t}\n\t}\n}\n\n// ConnectorsInfo get all enabled connectors\n// @Summary get all enabled connectors\n// @Description get all enabled connectors\n// @Tags PluginConnector\n// @Security ApiKeyAuth\n// @Produce  json\n// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp}\n// @Router /answer/api/v1/connector/info [get]\nfunc (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {\n\tgeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tresp := make([]*schema.ConnectorInfoResp, 0)\n\t_ = plugin.CallConnector(func(fn plugin.Connector) error {\n\t\tconnectorName := fn.ConnectorName()\n\t\tresp = append(resp, &schema.ConnectorInfoResp{\n\t\t\tName: connectorName.Translate(ctx),\n\t\t\tIcon: fn.ConnectorLogoSVG(),\n\t\t\tLink: fmt.Sprintf(\"%s%s%s%s\", general.SiteUrl,\n\t\t\t\tcommonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),\n\t\t})\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// ExternalLoginBindingUserSendEmail external login binding user send email\n// @Summary external login binding user send email\n// @Description external login binding user send email\n// @Tags PluginConnector\n// @Accept json\n// @Produce json\n// @Param data body schema.ExternalLoginBindingUserSendEmailReq  true \"external login binding user send email\"\n// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp}\n// @Router /answer/api/v1/connector/binding/email [post]\nfunc (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {\n\treq := &schema.ExternalLoginBindingUserSendEmailReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// ConnectorsUserInfo get all connectors info about user\n// @Summary get all connectors info about user\n// @Description get all connectors info about user\n// @Tags PluginConnector\n// @Security ApiKeyAuth\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp}\n// @Router /answer/api/v1/connector/user/info [get]\nfunc (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) {\n\tgeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\n\tuserInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tuserExternalLoginMapping := make(map[string]string)\n\tfor _, userInfo := range userInfoList {\n\t\tuserExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID\n\t}\n\n\tresp := make([]*schema.ConnectorUserInfoResp, 0)\n\t_ = plugin.CallConnector(func(fn plugin.Connector) error {\n\t\texternalID := userExternalLoginMapping[fn.ConnectorSlugName()]\n\t\tconnectorName := fn.ConnectorName()\n\t\tresp = append(resp, &schema.ConnectorUserInfoResp{\n\t\t\tName: connectorName.Translate(ctx),\n\t\t\tIcon: fn.ConnectorLogoSVG(),\n\t\t\tLink: fmt.Sprintf(\"%s%s%s%s\", general.SiteUrl,\n\t\t\t\tcommonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),\n\t\t\tBinding:    len(externalID) > 0,\n\t\t\tExternalID: externalID,\n\t\t})\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// ExternalLoginUnbinding unbind external user login\n// @Summary unbind external user login\n// @Description unbind external user login\n// @Tags PluginConnector\n// @Security ApiKeyAuth\n// @Accept json\n// @Produce json\n// @Param data body schema.ExternalLoginUnbindingReq true \"ExternalLoginUnbindingReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/connector/user/unbinding [delete]\nfunc (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) {\n\treq := &schema.ExternalLoginUnbindingReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport \"github.com/google/wire\"\n\n// ProviderSetController is controller providers.\nvar ProviderSetController = wire.NewSet(\n\tNewLangController,\n\tNewCommentController,\n\tNewReportController,\n\tNewVoteController,\n\tNewTagController,\n\tNewFollowController,\n\tNewCollectionController,\n\tNewUserController,\n\tNewQuestionController,\n\tNewAnswerController,\n\tNewSearchController,\n\tNewRevisionController,\n\tNewRankController,\n\tNewReasonController,\n\tNewNotificationController,\n\tNewSiteInfoController,\n\tNewDashboardController,\n\tNewUploadController,\n\tNewActivityController,\n\tNewTemplateController,\n\tNewConnectorController,\n\tNewUserCenterController,\n\tNewPermissionController,\n\tNewUserPluginController,\n\tNewReviewController,\n\tNewCaptchaController,\n\tNewMetaController,\n\tNewEmbedController,\n\tNewBadgeController,\n\tNewRenderController,\n\tNewSidebarController,\n\tNewMCPController,\n\tNewAIController,\n\tNewAIConversationController,\n)\n"
  },
  {
    "path": "internal/controller/dashboard_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/service/dashboard\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype DashboardController struct {\n\tdashboardService dashboard.DashboardService\n}\n\n// NewDashboardController new controller\nfunc NewDashboardController(\n\tdashboardService dashboard.DashboardService,\n) *DashboardController {\n\treturn &DashboardController{\n\t\tdashboardService: dashboardService,\n\t}\n}\n\n// DashboardInfo godoc\n// @Summary DashboardInfo\n// @Description DashboardInfo\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Router /answer/admin/api/dashboard [get]\n// @Success 200 {object} handler.RespBody\nfunc (ac *DashboardController) DashboardInfo(ctx *gin.Context) {\n\tinfo, err := ac.dashboardService.Statistical(ctx)\n\thandler.HandleResponse(ctx, err, gin.H{\n\t\t\"info\": info,\n\t})\n}\n"
  },
  {
    "path": "internal/controller/embed_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype EmbedController struct {\n}\n\nfunc NewEmbedController() *EmbedController {\n\treturn &EmbedController{}\n}\n\n// GetEmbedConfig get embed plugin config\n// @Summary get embed plugin config\n// @Description get embed plugin config\n// @Tags Plugin\n// @Accept json\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]plugin.EmbedConfig}\n// @Router /answer/api/v1/embed/config [get]\nfunc (c *EmbedController) GetEmbedConfig(ctx *gin.Context) {\n\tresp := make([]*plugin.EmbedConfig, 0)\n\n\terr := plugin.CallEmbed(func(embed plugin.Embed) (err error) {\n\t\tresp, err = embed.GetEmbedConfigs(ctx)\n\t\treturn err\n\t})\n\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/follow_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/follow\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/jinzhu/copier\"\n)\n\n// FollowController activity controller\ntype FollowController struct {\n\tfollowService *follow.FollowService\n}\n\n// NewFollowController new controller\nfunc NewFollowController(followService *follow.FollowService) *FollowController {\n\treturn &FollowController{followService: followService}\n}\n\n// Follow godoc\n// @Summary follow object or cancel follow operation\n// @Description follow object or cancel follow operation\n// @Tags Activity\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.FollowReq true \"follow\"\n// @Success 200 {object} handler.RespBody{data=schema.FollowResp}\n// @Router /answer/api/v1/follow [post]\nfunc (fc *FollowController) Follow(ctx *gin.Context) {\n\treq := &schema.FollowReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\tdto := &schema.FollowDTO{}\n\t_ = copier.Copy(dto, req)\n\tdto.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := fc.followService.Follow(ctx, dto)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, schema.ErrTypeToast)\n\t} else {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t}\n}\n\n// UpdateFollowTags update user follow tags\n// @Summary update user follow tags\n// @Description update user follow tags\n// @Tags Activity\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateFollowTagsReq true \"follow\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/follow/tags [put]\nfunc (fc *FollowController) UpdateFollowTags(ctx *gin.Context) {\n\treq := &schema.UpdateFollowTagsReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := fc.followService.UpdateFollowTags(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/lang_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\ntype LangController struct {\n\ttranslator      i18n.Translator\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n}\n\n// NewLangController new language controller.\nfunc NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController {\n\treturn &LangController{translator: tr, siteInfoService: siteInfoService}\n}\n\n// GetLangMapping get language config mapping\n// @Summary get language config mapping\n// @Description get language config mapping\n// @Tags Lang\n// @Param Accept-Language header string true \"Accept-Language\"\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/language/config [get]\nfunc (u *LangController) GetLangMapping(ctx *gin.Context) {\n\tdata, _ := u.translator.Dump(handler.GetLangByCtx(ctx))\n\tvar resp map[string]any\n\t_ = json.Unmarshal(data, &resp)\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetAdminLangOptions Get language options\n// @Summary Get language options\n// @Description Get language options\n// @Security ApiKeyAuth\n// @Tags Lang\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/language/options [get]\nfunc (u *LangController) GetAdminLangOptions(ctx *gin.Context) {\n\thandler.HandleResponse(ctx, nil, translator.LanguageOptions)\n}\n\n// GetUserLangOptions Get language options\n// @Summary Get language options\n// @Description Get language options\n// @Tags Lang\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/language/options [get]\nfunc (u *LangController) GetUserLangOptions(ctx *gin.Context) {\n\tsiteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\toptions := translator.LanguageOptions\n\tif len(siteInterfaceResp.Language) > 0 {\n\t\tdefaultOption := []*translator.LangOption{\n\t\t\t{Label: translator.DefaultLangOption, Value: translator.DefaultLangOption},\n\t\t}\n\t\toptions = append(defaultOption, options...)\n\t}\n\thandler.HandleResponse(ctx, nil, options)\n}\n"
  },
  {
    "path": "internal/controller/mcp_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttagcommonser \"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype MCPController struct {\n\tsearchService    *content.SearchService\n\tsiteInfoService  siteinfo_common.SiteInfoCommonService\n\ttagCommonService *tagcommonser.TagCommonService\n\tquestioncommon   *questioncommon.QuestionCommon\n\tcommentRepo      comment.CommentRepo\n\tuserCommon       *usercommon.UserCommon\n\tanswerRepo       answercommon.AnswerRepo\n\tfeatureToggleSvc *feature_toggle.FeatureToggleService\n}\n\n// NewMCPController new site info controller.\nfunc NewMCPController(\n\tsearchService *content.SearchService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\ttagCommonService *tagcommonser.TagCommonService,\n\tquestioncommon *questioncommon.QuestionCommon,\n\tcommentRepo comment.CommentRepo,\n\tuserCommon *usercommon.UserCommon,\n\tanswerRepo answercommon.AnswerRepo,\n\tfeatureToggleSvc *feature_toggle.FeatureToggleService,\n) *MCPController {\n\treturn &MCPController{\n\t\tsearchService:    searchService,\n\t\tsiteInfoService:  siteInfoService,\n\t\ttagCommonService: tagCommonService,\n\t\tquestioncommon:   questioncommon,\n\t\tcommentRepo:      commentRepo,\n\t\tuserCommon:       userCommon,\n\t\tanswerRepo:       answerRepo,\n\t\tfeatureToggleSvc: featureToggleSvc,\n\t}\n}\n\nfunc (c *MCPController) ensureMCPEnabled(ctx context.Context) error {\n\tif c.featureToggleSvc == nil {\n\t\treturn nil\n\t}\n\treturn c.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureMCP)\n}\n\nfunc (c *MCPController) MCPQuestionsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tsearchResp, err := c.searchService.Search(ctx, &schema.SearchDTO{\n\t\t\tQuery: cond.ToQueryString() + \" is:question\",\n\t\t\tPage:  1,\n\t\t\tSize:  5,\n\t\t\tOrder: \"newest\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp := make([]*schema.MCPSearchQuestionInfoResp, 0)\n\t\tfor _, question := range searchResp.SearchResults {\n\t\t\tt := &schema.MCPSearchQuestionInfoResp{\n\t\t\t\tQuestionID: question.Object.QuestionID,\n\t\t\t\tTitle:      question.Object.Title,\n\t\t\t\tContent:    question.Object.Excerpt,\n\t\t\t\tLink:       fmt.Sprintf(\"%s/questions/%s\", siteGeneral.SiteUrl, question.Object.QuestionID),\n\t\t\t}\n\t\t\tresp = append(resp, t)\n\t\t}\n\n\t\tdata, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(data)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPQuestionDetailHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchQuestionDetail(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tquestion, err := c.questioncommon.Info(ctx, cond.QuestionID, \"\")\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get question failed: %v\", err)\n\t\t\treturn mcp.NewToolResultText(\"No question found.\"), nil\n\t\t}\n\n\t\tresp := &schema.MCPSearchQuestionInfoResp{\n\t\t\tQuestionID: question.ID,\n\t\t\tTitle:      question.Title,\n\t\t\tContent:    question.Content,\n\t\t\tLink:       fmt.Sprintf(\"%s/questions/%s\", siteGeneral.SiteUrl, question.ID),\n\t\t}\n\t\tres, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(res)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchAnswerCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(cond.QuestionID) > 0 {\n\t\t\tanswerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID})\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"get answers failed: %v\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresp := make([]*schema.MCPSearchAnswerInfoResp, 0)\n\t\t\tfor _, answer := range answerList {\n\t\t\t\tt := &schema.MCPSearchAnswerInfoResp{\n\t\t\t\t\tQuestionID:    answer.QuestionID,\n\t\t\t\t\tAnswerID:      answer.ID,\n\t\t\t\t\tAnswerContent: answer.OriginalText,\n\t\t\t\t\tLink:          fmt.Sprintf(\"%s/questions/%s/answers/%s\", siteGeneral.SiteUrl, answer.QuestionID, answer.ID),\n\t\t\t\t}\n\t\t\t\tresp = append(resp, t)\n\t\t\t}\n\t\t\tdata, _ := json.Marshal(resp)\n\t\t\treturn mcp.NewToolResultText(string(data)), nil\n\t\t}\n\n\t\tanswerList, err := c.answerRepo.GetAnswerList(ctx, &entity.Answer{QuestionID: cond.QuestionID})\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get answers failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tresp := make([]*schema.MCPSearchAnswerInfoResp, 0)\n\t\tfor _, answer := range answerList {\n\t\t\tt := &schema.MCPSearchAnswerInfoResp{\n\t\t\t\tQuestionID:    answer.QuestionID,\n\t\t\t\tAnswerID:      answer.ID,\n\t\t\t\tAnswerContent: answer.OriginalText,\n\t\t\t\tLink:          fmt.Sprintf(\"%s/questions/%s/answers/%s\", siteGeneral.SiteUrl, answer.QuestionID, answer.ID),\n\t\t\t}\n\t\t\tresp = append(resp, t)\n\t\t}\n\t\tdata, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(data)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPCommentsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchCommentCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdto := &comment.CommentQuery{\n\t\t\tPageCond:  pager.PageCond{Page: 1, PageSize: 5},\n\t\t\tQueryCond: \"newest\",\n\t\t\tObjectID:  cond.ObjectID,\n\t\t}\n\t\tcommentList, total, err := c.commentRepo.GetCommentPage(ctx, dto)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif total == 0 {\n\t\t\treturn mcp.NewToolResultText(\"No comments found.\"), nil\n\t\t}\n\n\t\tresp := make([]*schema.MCPSearchCommentInfoResp, 0)\n\t\tfor _, comment := range commentList {\n\t\t\tt := &schema.MCPSearchCommentInfoResp{\n\t\t\t\tCommentID: comment.ID,\n\t\t\t\tContent:   comment.OriginalText,\n\t\t\t\tObjectID:  comment.ObjectID,\n\t\t\t\tLink:      fmt.Sprintf(\"%s/comments/%s\", siteGeneral.SiteUrl, comment.ID),\n\t\t\t}\n\t\t\tresp = append(resp, t)\n\t\t}\n\t\tdata, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(data)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPTagsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchTagCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttags, total, err := c.tagCommonService.GetTagPage(ctx, 1, 10, &entity.Tag{DisplayName: cond.TagName}, \"newest\")\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get tags failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif total == 0 {\n\t\t\tres := strings.Builder{}\n\t\t\tres.WriteString(\"No tags found.\\n\")\n\t\t\treturn mcp.NewToolResultText(res.String()), nil\n\t\t}\n\n\t\tresp := make([]*schema.MCPSearchTagResp, 0)\n\t\tfor _, tag := range tags {\n\t\t\tt := &schema.MCPSearchTagResp{\n\t\t\t\tTagName:     tag.SlugName,\n\t\t\t\tDisplayName: tag.DisplayName,\n\t\t\t\tDescription: tag.OriginalText,\n\t\t\t\tLink:        fmt.Sprintf(\"%s/tags/%s\", siteGeneral.SiteUrl, tag.SlugName),\n\t\t\t}\n\t\t\tresp = append(resp, t)\n\t\t}\n\t\tdata, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(data)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPTagDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchTagCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttag, exist, err := c.tagCommonService.GetTagBySlugName(ctx, cond.TagName)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get tag failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn mcp.NewToolResultText(\"Tag not found.\"), nil\n\t\t}\n\n\t\tresp := &schema.MCPSearchTagResp{\n\t\t\tTagName:     tag.SlugName,\n\t\t\tDisplayName: tag.DisplayName,\n\t\t\tDescription: tag.OriginalText,\n\t\t\tLink:        fmt.Sprintf(\"%s/tags/%s\", siteGeneral.SiteUrl, tag.SlugName),\n\t\t}\n\t\tres, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(res)), nil\n\t}\n}\n\nfunc (c *MCPController) MCPUserDetailsHandler() func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\treturn func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\tif err := c.ensureMCPEnabled(ctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcond := schema.NewMCPSearchUserCond(request)\n\n\t\tsiteGeneral, err := c.siteInfoService.GetSiteGeneral(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tuser, exist, err := c.userCommon.GetUserBasicInfoByUserName(ctx, cond.Username)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get user failed: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn mcp.NewToolResultText(\"User not found.\"), nil\n\t\t}\n\n\t\tresp := &schema.MCPSearchUserInfoResp{\n\t\t\tUsername:    user.Username,\n\t\t\tDisplayName: user.DisplayName,\n\t\t\tAvatar:      user.Avatar,\n\t\t\tLink:        fmt.Sprintf(\"%s/users/%s\", siteGeneral.SiteUrl, user.Username),\n\t\t}\n\t\tres, _ := json.Marshal(resp)\n\t\treturn mcp.NewToolResultText(string(res)), nil\n\t}\n}\n"
  },
  {
    "path": "internal/controller/meta_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/meta\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype MetaController struct {\n\tmetaService *meta.MetaService\n}\n\nfunc NewMetaController(\n\tmetaService *meta.MetaService,\n) *MetaController {\n\treturn &MetaController{\n\t\tmetaService: metaService,\n\t}\n}\n\n// AddOrUpdateReaction add or update reaction\n// @Summary add or update reaction\n// @Description update reaction. if not exist, add one\n// @Tags Meta\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateReactionReq true \"reaction\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/meta/reaction [put]\nfunc (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) {\n\treq := &schema.UpdateReactionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := mc.metaService.AddOrUpdateReaction(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetReaction get reaction\n// @Summary get reaction\n// @Description get reaction for an object\n// @Tags Meta\n// @Accept json\n// @Produce json\n// @Param object_id query string true \"object_id\"\n// @Success 200 {object} handler.RespBody{data=schema.ReactionRespItem}\n// @Router /answer/api/v1/meta/reaction [get]\nfunc (mc *MetaController) GetReaction(ctx *gin.Context) {\n\treq := &schema.GetReactionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := mc.metaService.GetReactionByObjectId(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/notification_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/notification\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// NotificationController notification controller\ntype NotificationController struct {\n\tnotificationService *notification.NotificationService\n\trankService         *rank.RankService\n}\n\n// NewNotificationController new controller\nfunc NewNotificationController(\n\tnotificationService *notification.NotificationService,\n\trankService *rank.RankService,\n) *NotificationController {\n\treturn &NotificationController{\n\t\tnotificationService: notificationService,\n\t\trankService:         rankService,\n\t}\n}\n\n// GetRedDot\n// @Summary GetRedDot\n// @Description GetRedDot\n// @Tags Notification\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/notification/status [get]\nfunc (nc *NotificationController) GetRedDot(ctx *gin.Context) {\n\treq := &schema.GetRedDot{}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAudit,\n\t\tpermission.AnswerAudit,\n\t\tpermission.TagAudit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanReviewQuestion = canList[0]\n\treq.CanReviewAnswer = canList[1]\n\treq.CanReviewTag = canList[2]\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\n\tresp, err := nc.notificationService.GetRedDot(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// ClearRedDot\n// @Summary DelRedDot\n// @Description DelRedDot\n// @Tags Notification\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.NotificationClearRequest true \"NotificationClearRequest\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/notification/status [put]\nfunc (nc *NotificationController) ClearRedDot(ctx *gin.Context) {\n\treq := &schema.NotificationClearRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAudit,\n\t\tpermission.AnswerAudit,\n\t\tpermission.TagAudit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanReviewQuestion = canList[0]\n\treq.CanReviewAnswer = canList[1]\n\treq.CanReviewTag = canList[2]\n\n\tresp, err := nc.notificationService.ClearRedDot(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// ClearUnRead\n// @Summary ClearUnRead\n// @Description ClearUnRead\n// @Tags Notification\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.NotificationClearRequest true \"NotificationClearRequest\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/notification/read/state/all [put]\nfunc (nc *NotificationController) ClearUnRead(ctx *gin.Context) {\n\treq := &schema.NotificationClearRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\terr := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType)\n\thandler.HandleResponse(ctx, err, gin.H{})\n}\n\n// ClearIDUnRead\n// @Summary ClearUnRead\n// @Description ClearUnRead\n// @Tags Notification\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.NotificationClearIDRequest true \"NotificationClearIDRequest\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/notification/read/state [put]\nfunc (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {\n\treq := &schema.NotificationClearIDRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\terr := nc.notificationService.ClearIDUnRead(ctx, userID, req.ID)\n\thandler.HandleResponse(ctx, err, gin.H{})\n}\n\n// GetList get notification list\n// @Summary get notification list\n// @Description get notification list\n// @Tags Notification\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Param type query string true \"type\" Enums(inbox,achievement)\n// @Param inbox_type query string true \"inbox_type\" Enums(all,posts,invites,votes)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/notification/page [get]\nfunc (nc *NotificationController) GetList(ctx *gin.Context) {\n\treq := &schema.NotificationSearch{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := nc.notificationService.GetNotificationPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/permission_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype PermissionController struct {\n\trankService *rank.RankService\n}\n\n// NewPermissionController new language controller.\nfunc NewPermissionController(rankService *rank.RankService) *PermissionController {\n\treturn &PermissionController{rankService: rankService}\n}\n\n// GetPermission check user permission\n// @Summary check user permission\n// @Description check user permission\n// @Tags Permission\n// @Security ApiKeyAuth\n// @Param Authorization header string true \"access-token\"\n// @Produce json\n// @Param action query string true \"permission key\" Enums(question.add, question.edit, question.edit_without_review, question.delete, question.close, question.reopen, question.vote_up, question.vote_down, question.pin, question.unpin, question.hide, question.show, answer.add, answer.edit, answer.edit_without_review, answer.delete, answer.accept, answer.vote_up, answer.vote_down, answer.invite_someone_to_answer, comment.add, comment.edit, comment.delete, comment.vote_up, comment.vote_down, report.add, tag.add, tag.edit, tag.edit_slug_name, tag.edit_without_review, tag.delete, tag.synonym, link.url_limit, vote.detail, answer.audit, question.audit, tag.audit, tag.use_reserved_tag)\n// @Success 200 {object} handler.RespBody{data=map[string]bool}\n// @Router /answer/api/v1/permission [get]\nfunc (u *PermissionController) GetPermission(ctx *gin.Context) {\n\treq := &schema.GetPermissionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tops, requireRanks, err := u.rankService.CheckOperationPermissionsForRanks(ctx, userID, req.Actions)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\tmapping := make(map[string]*schema.GetPermissionResp, len(ops))\n\tfor i, action := range req.Actions {\n\t\tt := &schema.GetPermissionResp{HasPermission: ops[i]}\n\t\tt.TrTip(lang, requireRanks[i])\n\t\tmapping[action] = t\n\t}\n\thandler.HandleResponse(ctx, err, mapping)\n}\n"
  },
  {
    "path": "internal/controller/plugin_captcha_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// CaptchaController comment controller\ntype CaptchaController struct {\n}\n\n// NewCaptchaController new controller\nfunc NewCaptchaController() *CaptchaController {\n\treturn &CaptchaController{}\n}\n\ntype GetCaptchaConfigResp struct {\n\tSlugName string         `json:\"slug_name\"`\n\tConfig   map[string]any `json:\"config\"`\n}\n\n// GetCaptchaConfig get captcha config\nfunc (uc *CaptchaController) GetCaptchaConfig(ctx *gin.Context) {\n\tresp := &GetCaptchaConfigResp{}\n\t_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {\n\t\tresp.SlugName = fn.Info().SlugName\n\t\t_ = json.Unmarshal([]byte(fn.GetConfig()), &resp.Config)\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n"
  },
  {
    "path": "internal/controller/plugin_sidebar_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// SidebarController is the controller for the sidebar plugin.\ntype SidebarController struct{}\n\n// NewSidebarController creates a new instance of SidebarController.\nfunc NewSidebarController() *SidebarController {\n\treturn &SidebarController{}\n}\n\n// GetSidebarConfig retrieves the sidebar configuration from the registered sidebar plugins.\nfunc (uc *SidebarController) GetSidebarConfig(ctx *gin.Context) {\n\tresp := &plugin.SidebarConfig{}\n\t_ = plugin.CallSidebar(func(fn plugin.Sidebar) error {\n\t\tcfg, err := fn.GetSidebarConfig()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tresp = cfg\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n"
  },
  {
    "path": "internal/controller/plugin_user_center_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nconst (\n\tUserCenterLoginRouter          = \"/user-center/login/redirect\"\n\tUserCenterSignUpRedirectRouter = \"/user-center/sign-up/redirect\"\n)\n\n// UserCenterController comment controller\ntype UserCenterController struct {\n\tuserCenterLoginService *user_external_login.UserCenterLoginService\n\tsiteInfoService        siteinfo_common.SiteInfoCommonService\n}\n\n// NewUserCenterController new controller\nfunc NewUserCenterController(\n\tuserCenterLoginService *user_external_login.UserCenterLoginService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n) *UserCenterController {\n\treturn &UserCenterController{\n\t\tuserCenterLoginService: userCenterLoginService,\n\t\tsiteInfoService:        siteInfoService,\n\t}\n}\n\n// UserCenterAgent get user center agent info\nfunc (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {\n\tresp := &schema.UserCenterAgentResp{}\n\tresp.Enabled = plugin.UserCenterEnabled()\n\tif !resp.Enabled {\n\t\thandler.HandleResponse(ctx, nil, resp)\n\t\treturn\n\t}\n\tsiteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site info failed: %v\", err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\n\tresp.AgentInfo = &schema.AgentInfo{}\n\tresp.AgentInfo.LoginRedirectURL = fmt.Sprintf(\"%s%s%s\", siteGeneral.SiteUrl,\n\t\tcommonRouterPrefix, UserCenterLoginRouter)\n\tresp.AgentInfo.SignUpRedirectURL = fmt.Sprintf(\"%s%s%s\", siteGeneral.SiteUrl,\n\t\tcommonRouterPrefix, UserCenterSignUpRedirectRouter)\n\n\t_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {\n\t\tinfo := uc.Description()\n\t\tresp.AgentInfo.Name = info.Name\n\t\tresp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)\n\t\tresp.AgentInfo.Icon = info.Icon\n\t\tresp.AgentInfo.Url = info.Url\n\t\tresp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)\n\t\tresp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem\n\t\titems := uc.ControlCenterItems()\n\t\tfor _, item := range items {\n\t\t\tresp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{\n\t\t\t\tName:  item.Name,\n\t\t\t\tLabel: item.Label,\n\t\t\t\tUrl:   item.Url,\n\t\t\t})\n\t\t}\n\t\treturn nil\n\t})\n\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// UserCenterPersonalBranding get user center personal user info\nfunc (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {\n\treq := &schema.GetOtherUserInfoByUsernameReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\nfunc (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {\n\tvar redirectURL string\n\t_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {\n\t\tinfo := userCenter.Description()\n\t\tredirectURL = info.LoginRedirectURL\n\t\treturn nil\n\t})\n\tctx.Redirect(http.StatusFound, redirectURL)\n}\n\nfunc (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {\n\tvar redirectURL string\n\t_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {\n\t\tinfo := userCenter.Description()\n\t\tredirectURL = info.LoginRedirectURL\n\t\treturn nil\n\t})\n\tctx.Redirect(http.StatusFound, redirectURL)\n}\n\nfunc (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {\n\tsiteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site info failed: %v\", err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\tctx.Redirect(http.StatusFound, \"/404\")\n\t\treturn\n\t}\n\tuserInfo, err := userCenter.LoginCallback(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tif !ctx.IsAborted() {\n\t\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\t}\n\t\treturn\n\t}\n\n\tresp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)\n\tif err != nil {\n\t\tlog.Errorf(\"external login failed: %v\", err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\tif len(resp.ErrMsg) > 0 {\n\t\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"/50x?title=%s&msg=%s\", resp.ErrTitle, resp.ErrMsg))\n\t\treturn\n\t}\n\tuserCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)\n\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"%s/users/auth-landing?access_token=%s\",\n\t\tsiteGeneral.SiteUrl, resp.AccessToken))\n}\n\nfunc (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {\n\tsiteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site info failed: %v\", err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\tctx.Redirect(http.StatusFound, \"/404\")\n\t\treturn\n\t}\n\tuserInfo, err := userCenter.SignUpCallback(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\n\tresp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)\n\tif err != nil {\n\t\tlog.Errorf(\"external login failed: %v\", err)\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t\treturn\n\t}\n\tif len(resp.ErrMsg) > 0 {\n\t\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"/50x?title=%s&msg=%s\", resp.ErrTitle, resp.ErrMsg))\n\t\treturn\n\t}\n\tuserCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)\n\tctx.Redirect(http.StatusFound, fmt.Sprintf(\"%s/users/auth-landing?access_token=%s\",\n\t\tsiteGeneral.SiteUrl, resp.AccessToken))\n}\n\n// UserCenterUserSettings user center user settings\nfunc (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserCenterAdminFunctionAgent user center admin function agent\nfunc (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {\n\tresp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/question_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// QuestionController question controller\ntype QuestionController struct {\n\tquestionService     *content.QuestionService\n\tanswerService       *content.AnswerService\n\trankService         *rank.RankService\n\tsiteInfoService     siteinfo_common.SiteInfoCommonService\n\tactionService       *action.CaptchaService\n\trateLimitMiddleware *middleware.RateLimitMiddleware\n}\n\n// NewQuestionController new controller\nfunc NewQuestionController(\n\tquestionService *content.QuestionService,\n\tanswerService *content.AnswerService,\n\trankService *rank.RankService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tactionService *action.CaptchaService,\n\trateLimitMiddleware *middleware.RateLimitMiddleware,\n) *QuestionController {\n\treturn &QuestionController{\n\t\tquestionService:     questionService,\n\t\tanswerService:       answerService,\n\t\trankService:         rankService,\n\t\tsiteInfoService:     siteInfoService,\n\t\tactionService:       actionService,\n\t\trateLimitMiddleware: rateLimitMiddleware,\n\t}\n}\n\n// RemoveQuestion delete question\n// @Summary delete question\n// @Description delete question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.RemoveQuestionReq true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router  /answer/api/v1/question [delete]\nfunc (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {\n\treq := &schema.RemoveQuestionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetIsAdminFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tcan, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\terr = qc.questionService.RemoveQuestion(ctx, req)\n\tif !isAdmin {\n\t\tqc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// OperationQuestion Operation question\n// @Summary Operation question\n// @Description Operation question \\n operation [pin unpin hide show]\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.OperationQuestionReq true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router  /answer/api/v1/question/operation [put]\nfunc (qc *QuestionController) OperationQuestion(ctx *gin.Context) {\n\treq := &schema.OperationQuestionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionPin,\n\t\tpermission.QuestionUnPin,\n\t\tpermission.QuestionHide,\n\t\tpermission.QuestionShow,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanPin = canList[0]\n\treq.CanList = canList[1]\n\tif (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\tif (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\terr = qc.questionService.OperationQuestion(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// CloseQuestion Close question\n// @Summary Close question\n// @Description Close question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.CloseQuestionReq true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router  /answer/api/v1/question/status [put]\nfunc (qc *QuestionController) CloseQuestion(ctx *gin.Context) {\n\treq := &schema.CloseQuestionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcan, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = qc.questionService.CloseQuestion(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// ReopenQuestion reopen question\n// @Summary reopen question\n// @Description reopen question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.ReopenQuestionReq true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question/reopen [put]\nfunc (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {\n\treq := &schema.ReopenQuestionReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcan, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = qc.questionService.ReopenQuestion(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetQuestion get question details\n// @Summary get question details\n// @Description get question details\n// @Tags Question\n// @Accept  json\n// @Produce  json\n// @Param id query string true \"Question TagID\"  default(1)\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/question/info [get]\nfunc (qc *QuestionController) GetQuestion(ctx *gin.Context) {\n\tid := ctx.Query(\"id\")\n\tid = uid.DeShortID(id)\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\treq := schema.QuestionPermission{}\n\tcanList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{\n\t\tpermission.QuestionEdit,\n\t\tpermission.QuestionDelete,\n\t\tpermission.QuestionClose,\n\t\tpermission.QuestionReopen,\n\t\tpermission.QuestionPin,\n\t\tpermission.QuestionUnPin,\n\t\tpermission.QuestionHide,\n\t\tpermission.QuestionShow,\n\t\tpermission.AnswerInviteSomeoneToAnswer,\n\t\tpermission.QuestionUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tobjectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)\n\n\treq.CanEdit = canList[0] || objectOwner\n\treq.CanDelete = canList[1]\n\treq.CanClose = canList[2]\n\treq.CanReopen = canList[3]\n\treq.CanPin = canList[4]\n\treq.CanUnPin = canList[5]\n\treq.CanHide = canList[6]\n\treq.CanShow = canList[7]\n\treq.CanInviteOtherToAnswer = canList[8]\n\treq.CanRecover = canList[9]\n\n\tinfo, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tinfo.ID = uid.EnShortID(info.ID)\n\t}\n\thandler.HandleResponse(ctx, nil, info)\n}\n\n// GetQuestionInviteUserInfo get question invite user info\n// @Summary get question invite user info\n// @Description get question invite user info\n// @Tags Question\n// @Accept  json\n// @Produce  json\n// @Param id query string true \"Question ID\"  default(1)\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/question/invite [get]\nfunc (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) {\n\tquestionID := uid.DeShortID(ctx.Query(\"id\"))\n\tresp, err := qc.questionService.InviteUserInfo(ctx, questionID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// SimilarQuestion godoc\n// @Summary Search Similar Question\n// @Description Search Similar Question\n// @Tags Question\n// @Accept  json\n// @Produce  json\n// @Param question_id query string true \"question_id\"  default()\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/question/similar/tag [get]\nfunc (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {\n\tquestionID := ctx.Query(\"question_id\")\n\tquestionID = uid.DeShortID(questionID)\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tlist, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, gin.H{\n\t\t\"list\":  list,\n\t\t\"count\": count,\n\t})\n}\n\n// QuestionPage get questions by page\n// @Summary get questions by page\n// @Description get questions by page\n// @Tags Question\n// @Accept  json\n// @Produce  json\n// @Param data body schema.QuestionPageReq  true \"QuestionPageReq\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}\n// @Router /answer/api/v1/question/page [get]\nfunc (qc *QuestionController) QuestionPage(ctx *gin.Context) {\n\treq := &schema.QuestionPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tquestions, total, err := qc.questionService.GetQuestionPage(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif pager.ValPageOutOfRange(total, req.Page, req.PageSize) {\n\t\thandler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))\n}\n\n// QuestionRecommendPage get recommend questions by page\n// @Summary get recommend questions by page\n// @Description get recommend questions by page\n// @Tags Question\n// @Accept  json\n// @Produce  json\n// @Param data body schema.QuestionPageReq  true \"QuestionPageReq\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}\n// @Router /answer/api/v1/question/recommend/page [get]\nfunc (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) {\n\treq := &schema.QuestionPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tif req.LoginUserID == \"\" {\n\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\treturn\n\t}\n\n\tquestions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))\n}\n\n// AddQuestion add question\n// @Summary add question\n// @Description add question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.QuestionAdd true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question [post]\nfunc (qc *QuestionController) AddQuestion(ctx *gin.Context) {\n\treq := &schema.QuestionAdd{}\n\terrFields := handler.BindAndCheckReturnErr(ctx, req)\n\tif ctx.IsAborted() {\n\t\treturn\n\t}\n\treject, rejectKey := qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)\n\tif reject {\n\t\treturn\n\t}\n\tdefer func() {\n\t\t// If status is not 200 means that the bad request has been returned, so the record should be cleared\n\t\tif ctx.Writer.Status() != http.StatusOK {\n\t\t\tqc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)\n\t\t}\n\t}()\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{\n\t\tpermission.QuestionAdd,\n\t\tpermission.QuestionEdit,\n\t\tpermission.QuestionDelete,\n\t\tpermission.QuestionClose,\n\t\tpermission.QuestionReopen,\n\t\tpermission.TagUseReservedTag,\n\t\tpermission.TagAdd,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tlinkUrlLimitUser := canList[7]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\treq.CanAdd = canList[0]\n\treq.CanEdit = canList[1]\n\treq.CanDelete = canList[2]\n\treq.CanClose = canList[3]\n\treq.CanReopen = canList[4]\n\treq.CanUseReservedTag = canList[5]\n\treq.CanAddTag = canList[6]\n\tif !req.CanAdd {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\t// can add tag\n\thasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !req.CanAddTag && hasNewTag {\n\t\tlang := handler.GetLangByCtx(ctx)\n\t\tmsg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]})\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)\n\t\treturn\n\t}\n\n\terrList, err := qc.questionService.CheckAddQuestion(ctx, req)\n\tif err != nil {\n\t\terrlist, ok := errList.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\n\treq.UserAgent = ctx.GetHeader(\"User-Agent\")\n\treq.IP = ctx.ClientIP()\n\n\tresp, err := qc.questionService.AddQuestion(ctx, req)\n\tif err != nil {\n\t\terrlist, ok := resp.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tqc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// AddQuestionByAnswer add question\n// @Summary add question and answer\n// @Description add question and answer\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.QuestionAddByAnswer true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question/answer [post]\nfunc (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {\n\treq := &schema.QuestionAddByAnswer{}\n\terrFields := handler.BindAndCheckReturnErr(ctx, req)\n\tif ctx.IsAborted() {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAdd,\n\t\tpermission.QuestionEdit,\n\t\tpermission.QuestionDelete,\n\t\tpermission.QuestionClose,\n\t\tpermission.QuestionReopen,\n\t\tpermission.TagUseReservedTag,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tlinkUrlLimitUser := canList[6]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\treq.CanAdd = canList[0]\n\treq.CanEdit = canList[1]\n\treq.CanDelete = canList[2]\n\treq.CanClose = canList[3]\n\treq.CanReopen = canList[4]\n\treq.CanUseReservedTag = canList[5]\n\tif !req.CanAdd {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\tquestionReq := new(schema.QuestionAdd)\n\terr = copier.Copy(questionReq, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil)\n\t\treturn\n\t}\n\terrList, err := qc.questionService.CheckAddQuestion(ctx, questionReq)\n\tif err != nil {\n\t\terrlist, ok := errList.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\n\treq.UserAgent = ctx.GetHeader(\"User-Agent\")\n\treq.IP = ctx.ClientIP()\n\tresp, err := qc.questionService.AddQuestion(ctx, questionReq)\n\tif err != nil {\n\t\terrlist, ok := resp.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tqc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID)\n\t}\n\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\t// add the question id to the answer\n\tquestionInfo, ok := resp.(*schema.QuestionInfoResp)\n\tif ok {\n\t\tanswerReq := &schema.AnswerAddReq{}\n\t\tanswerReq.QuestionID = uid.DeShortID(questionInfo.ID)\n\t\tanswerReq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\t\tanswerReq.Content = req.AnswerContent\n\t\tanswerReq.HTML = req.AnswerHTML\n\t\tanswerID, err := qc.answerService.Insert(ctx, answerReq)\n\t\tif err != nil {\n\t\t\thandler.HandleResponse(ctx, err, nil)\n\t\t\treturn\n\t\t}\n\t\tinfo, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID)\n\t\tif err != nil {\n\t\t\thandler.HandleResponse(ctx, err, nil)\n\t\t\treturn\n\t\t}\n\t\tif !has {\n\t\t\thandler.HandleResponse(ctx, nil, nil)\n\t\t\treturn\n\t\t}\n\t\thandler.HandleResponse(ctx, err, gin.H{\n\t\t\t\"info\":     info,\n\t\t\t\"question\": questionInfo,\n\t\t})\n\t\treturn\n\t}\n\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateQuestion update question\n// @Summary update question\n// @Description update question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.QuestionUpdate true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question [put]\nfunc (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {\n\treq := &schema.QuestionUpdate{}\n\terrFields := handler.BindAndCheckReturnErr(ctx, req)\n\tif ctx.IsAborted() {\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{\n\t\tpermission.QuestionEdit,\n\t\tpermission.QuestionDelete,\n\t\tpermission.QuestionEditWithoutReview,\n\t\tpermission.TagUseReservedTag,\n\t\tpermission.TagAdd,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tlinkUrlLimitUser := canList[5]\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tcaptchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tobjectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)\n\treq.CanEdit = canList[0] || objectOwner\n\treq.CanDelete = canList[1]\n\treq.NoNeedReview = canList[2] || objectOwner\n\treq.CanUseReservedTag = canList[3]\n\treq.CanAddTag = canList[4]\n\tif !req.CanEdit {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terrlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req)\n\tif err != nil {\n\t\terrFields = append(errFields, errlist...)\n\t}\n\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\n\t// can add tag\n\thasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !req.CanAddTag && hasNewTag {\n\t\tlang := handler.GetLangByCtx(ctx)\n\t\tmsg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[4]})\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)\n\t\treturn\n\t}\n\n\tresp, err := qc.questionService.UpdateQuestion(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t\treturn\n\t}\n\trespInfo, ok := resp.(*schema.QuestionInfoResp)\n\tif !ok {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t\treturn\n\t}\n\tif !isAdmin || !linkUrlLimitUser {\n\t\tqc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{UrlTitle: respInfo.UrlTitle, WaitForReview: !req.NoNeedReview})\n}\n\n// QuestionRecover recover deleted question\n// @Summary recover deleted question\n// @Description recover deleted question\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.QuestionRecoverReq true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question/recover [post]\nfunc (qc *QuestionController) QuestionRecover(ctx *gin.Context) {\n\treq := &schema.QuestionRecoverReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !canList[0] {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = qc.questionService.RecoverQuestion(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateQuestionInviteUser update question invite user\n// @Summary update question invite user\n// @Description update question invite user\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.QuestionUpdateInviteUser true \"question\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question/invite [put]\nfunc (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) {\n\treq := &schema.QuestionUpdateInviteUser{}\n\terrFields := handler.BindAndCheckReturnErr(ctx, req)\n\tif ctx.IsAborted() {\n\t\treturn\n\t}\n\tif len(errFields) > 0 {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)\n\t\treturn\n\t}\n\treq.ID = uid.DeShortID(req.ID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tcanList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.AnswerInviteSomeoneToAnswer,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\treq.CanInviteOtherToAnswer = canList[0]\n\tif !req.CanInviteOtherToAnswer {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\terr = qc.questionService.UpdateQuestionInviteUser(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !isAdmin {\n\t\tqc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, nil, nil)\n}\n\n// GetSimilarQuestions fuzzy query similar questions based on title\n// @Summary fuzzy query similar questions based on title\n// @Description fuzzy query similar questions based on title\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param title query string true \"title\"  default(string)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/question/similar [get]\nfunc (qc *QuestionController) GetSimilarQuestions(ctx *gin.Context) {\n\ttitle := ctx.Query(\"title\")\n\tresp, err := qc.questionService.GetQuestionsByTitle(ctx, title)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserTop godoc\n// @Summary UserTop\n// @Description UserTop\n// @Tags Question\n// @Accept json\n// @Produce json\n// @Param username query string true \"username\"  default(string)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/personal/qa/top [get]\nfunc (qc *QuestionController) UserTop(ctx *gin.Context) {\n\tuserName := ctx.Query(\"username\")\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tquestionList, answerList, err := qc.questionService.SearchUserTopList(ctx, userName, userID)\n\thandler.HandleResponse(ctx, err, gin.H{\n\t\t\"question\": questionList,\n\t\t\"answer\":   answerList,\n\t})\n}\n\n// PersonalQuestionPage list personal questions\n// @Summary list personal questions\n// @Description list personal questions\n// @Tags Personal\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param username query string true \"username\"  default(string)\n// @Param order query string true \"order\"  Enums(newest,score)\n// @Param page query string true \"page\"  default(0)\n// @Param page_size query string true \"page_size\" default(20)\n// @Success 200 {object} handler.RespBody\n// @Router /personal/question/page [get]\nfunc (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {\n\treq := &schema.PersonalQuestionPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\tresp, err := qc.questionService.PersonalQuestionPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// PersonalAnswerPage list personal answers\n// @Summary list personal answers\n// @Description list personal answers\n// @Tags Personal\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param username query string true \"username\"  default(string)\n// @Param order query string true \"order\"  Enums(newest,score)\n// @Param page query string true \"page\"  default(0)\n// @Param page_size query string true \"page_size\"  default(20)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/personal/answer/page [get]\nfunc (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {\n\treq := &schema.PersonalAnswerPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\tresp, err := qc.questionService.PersonalAnswerPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// PersonalCollectionPage list personal collections\n// @Summary list personal collections\n// @Description list personal collections\n// @Tags Collection\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query string true \"page\"  default(0)\n// @Param page_size query string true \"page_size\"  default(20)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/personal/collection/page [get]\nfunc (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {\n\treq := &schema.PersonalCollectionPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := qc.questionService.PersonalCollectionPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// AdminQuestionPage admin question page\n// @Summary AdminQuestionPage admin question page\n// @Description Status:[available,closed,deleted,pending]\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Param status query string false \"user status\" Enums(available, closed, deleted, pending)\n// @Param query query string false \"question id or title\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/question/page [get]\nfunc (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) {\n\treq := &schema.AdminQuestionPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := qc.questionService.AdminQuestionPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// AdminAnswerPage admin answer page\n// @Summary AdminAnswerPage admin answer page\n// @Description Status:[available,deleted,pending]\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Param status query string false \"user status\" Enums(available,deleted,pending)\n// @Param query query string false \"answer id or question title\"\n// @Param question_id query string false \"question id\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/answer/page [get]\nfunc (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) {\n\treq := &schema.AdminAnswerPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := qc.questionService.AdminAnswerPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// AdminUpdateQuestionStatus update question status\n// @Summary update question status\n// @Description update question status\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AdminUpdateQuestionStatusReq true \"AdminUpdateQuestionStatusReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/question/status [put]\nfunc (qc *QuestionController) AdminUpdateQuestionStatus(ctx *gin.Context) {\n\treq := &schema.AdminUpdateQuestionStatusReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := qc.questionService.AdminSetQuestionStatus(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetQuestionLink get question link\n// @Summary get question link\n// @Description get question link\n// @Tags Question\n// @Param data query schema.GetQuestionLinkReq  true \"GetQuestionLinkReq\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}\n// @Router /answer/api/v1/question/link [get]\nfunc (qc *QuestionController) GetQuestionLink(ctx *gin.Context) {\n\treq := &schema.GetQuestionLinkReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\tquestions, total, err := qc.questionService.GetQuestionLink(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))\n}\n"
  },
  {
    "path": "internal/controller/rank_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// RankController rank controller\ntype RankController struct {\n\trankService *rank.RankService\n}\n\n// NewRankController new controller\nfunc NewRankController(\n\trankService *rank.RankService) *RankController {\n\treturn &RankController{rankService: rankService}\n}\n\n// GetRankPersonalWithPage user personal rank list\n// @Summary user personal rank list\n// @Description user personal rank list\n// @Tags Rank\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Param username query string false \"username\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalPageResp}}\n// @Router /answer/api/v1/personal/rank/page [get]\nfunc (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) {\n\treq := &schema.GetRankPersonalWithPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := cc.rankService.GetRankPersonalPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/reason_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/reason\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// ReasonController answer controller\ntype ReasonController struct {\n\treasonService *reason.ReasonService\n}\n\n// NewReasonController new controller\nfunc NewReasonController(answerService *reason.ReasonService) *ReasonController {\n\treturn &ReasonController{reasonService: answerService}\n}\n\n// Reasons godoc\n// @Summary get reasons by object type and action\n// @Description get reasons by object type and action\n// @Tags reason\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param object_type query string true \"object_type\" Enums(question, answer, comment, user)\n// @Param action query string true \"action\" Enums(status, close, flag, review)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/reasons [get]\n// @Router /answer/admin/api/reasons [get]\nfunc (rc *ReasonController) Reasons(ctx *gin.Context) {\n\treq := &schema.ReasonReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treasons, err := rc.reasonService.GetReasons(ctx, *req)\n\thandler.HandleResponse(ctx, err, reasons)\n}\n"
  },
  {
    "path": "internal/controller/render_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype RenderController struct {\n}\n\nfunc NewRenderController() *RenderController {\n\treturn &RenderController{}\n}\n\n// GetRenderConfig godoc\n// @Summary GetRenderConfig\n// @Description GetRenderConfig\n// @Tags PluginRender\n// @Accept json\n// @Produce json\n// @Router /answer/api/v1/render/config [get]\n// @Success 200 {object} handler.RespBody{data=plugin.RenderConfig}\nfunc (c *RenderController) GetRenderConfig(ctx *gin.Context) {\n\tvar resp *plugin.RenderConfig\n\n\t_ = plugin.CallRender(func(render plugin.Render) (err error) {\n\t\tresp = render.GetRenderConfig(ctx)\n\t\treturn nil\n\t})\n\n\thandler.HandleResponse(ctx, nil, resp)\n}\n"
  },
  {
    "path": "internal/controller/report_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/report\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// ReportController report controller\ntype ReportController struct {\n\treportService *report.ReportService\n\trankService   *rank.RankService\n\tactionService *action.CaptchaService\n}\n\n// NewReportController new controller\nfunc NewReportController(\n\treportService *report.ReportService,\n\trankService *rank.RankService,\n\tactionService *action.CaptchaService,\n) *ReportController {\n\treturn &ReportController{\n\t\treportService: reportService,\n\t\trankService:   rankService,\n\t\tactionService: actionService,\n\t}\n}\n\n// AddReport add report\n// @Summary add report\n// @Description add report <br> source (question, answer, comment, user)\n// @Tags Report\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.AddReportReq true \"report\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/report [post]\nfunc (rc *ReportController) AddReport(ctx *gin.Context) {\n\treq := &schema.AddReportReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := rc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionReport, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tcan, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = rc.reportService.AddReport(ctx, req)\n\tif !isAdmin {\n\t\trc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionReport, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetUnreviewedReportPostPage get unreviewed report post page\n// @Summary get unreviewed report post page\n// @Description get unreviewed report post page\n// @Tags Report\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetReportListPageResp}}\n// @Router /answer/api/v1/report/unreviewed/post [get]\nfunc (rc *ReportController) GetUnreviewedReportPostPage(ctx *gin.Context) {\n\treq := &schema.GetUnreviewedReportPostPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\n\tresp, err := rc.reportService.GetUnreviewedReportPostPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// ReviewReport review report\n// @Summary review report\n// @Description review report\n// @Tags Report\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.ReviewReportReq true \"flag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/report/review [put]\nfunc (rc *ReportController) ReviewReport(ctx *gin.Context) {\n\treq := &schema.ReviewReportReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\tif !req.IsAdmin {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\treturn\n\t}\n\n\terr := rc.reportService.ReviewReport(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/review_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// ReviewController review controller\ntype ReviewController struct {\n\treviewService *review.ReviewService\n\trankService   *rank.RankService\n\tactionService *action.CaptchaService\n}\n\n// NewReviewController new controller\nfunc NewReviewController(\n\treviewService *review.ReviewService,\n\trankService *rank.RankService,\n\tactionService *action.CaptchaService,\n) *ReviewController {\n\treturn &ReviewController{\n\t\treviewService: reviewService,\n\t\trankService:   rankService,\n\t\tactionService: actionService,\n\t}\n}\n\n// GetUnreviewedPostPage get unreviewed post page\n// @Summary get unreviewed post page\n// @Description get unreviewed post page\n// @Tags Review\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page\"\n// @Param object_id query string false \"object_id\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedPostPageResp}}\n// @Router /answer/api/v1/review/pending/post/page [get]\nfunc (rc *ReviewController) GetUnreviewedPostPage(ctx *gin.Context) {\n\treq := &schema.GetUnreviewedPostPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\n\treq.ReviewerMapping = make(map[string]string)\n\t_ = plugin.CallReviewer(func(base plugin.Reviewer) error {\n\t\tinfo := base.Info()\n\t\treq.ReviewerMapping[info.SlugName] = info.Name.Translate(ctx)\n\t\treturn nil\n\t})\n\n\tresp, err := rc.reviewService.GetUnreviewedPostPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateReview update review\n// @Summary update review\n// @Description update review\n// @Tags Review\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateReviewReq true \"review\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/review/pending/post [put]\nfunc (rc *ReviewController) UpdateReview(ctx *gin.Context) {\n\treq := &schema.UpdateReviewReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\tif !req.IsAdmin {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\treturn\n\t}\n\n\terr := rc.reviewService.UpdateReview(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/revision_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// RevisionController revision controller\ntype RevisionController struct {\n\trevisionListService *content.RevisionService\n\trankService         *rank.RankService\n}\n\n// NewRevisionController new controller\nfunc NewRevisionController(\n\trevisionListService *content.RevisionService,\n\trankService *rank.RankService,\n) *RevisionController {\n\treturn &RevisionController{\n\t\trevisionListService: revisionListService,\n\t\trankService:         rankService,\n\t}\n}\n\n// GetRevisionList godoc\n// @Summary get revision list\n// @Description get revision list\n// @Tags Revision\n// @Produce json\n// @Param object_id query string true \"object id\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetRevisionResp}\n// @Router /answer/api/v1/revisions [get]\nfunc (rc *RevisionController) GetRevisionList(ctx *gin.Context) {\n\tobjectID := ctx.Query(\"object_id\")\n\tif objectID == \"0\" || objectID == \"\" {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)\n\t\treturn\n\t}\n\tobjectID = uid.DeShortID(objectID)\n\treq := &schema.GetRevisionListReq{\n\t\tObjectID: objectID,\n\t\tIsAdmin:  middleware.GetUserIsAdminModerator(ctx),\n\t\tUserID:   middleware.GetLoginUserIDFromContext(ctx),\n\t}\n\n\tresp, err := rc.revisionListService.GetRevisionList(ctx, req)\n\tlist := make([]schema.GetRevisionResp, 0)\n\tfor _, item := range resp {\n\t\tif item.Status == entity.RevisionNormalStatus || item.Status == entity.RevisionReviewPassStatus {\n\t\t\tlist = append(list, item)\n\t\t}\n\t}\n\thandler.HandleResponse(ctx, err, list)\n}\n\n// GetUnreviewedRevisionList godoc\n// @Summary get unreviewed revision list\n// @Description get unreviewed revision list\n// @Tags Revision\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query string true \"page id\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}}\n// @Router /answer/api/v1/revisions/unreviewed [get]\nfunc (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {\n\treq := &schema.RevisionSearch{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAudit,\n\t\tpermission.AnswerAudit,\n\t\tpermission.TagAudit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanReviewQuestion = canList[0]\n\treq.CanReviewAnswer = canList[1]\n\treq.CanReviewTag = canList[2]\n\n\tresp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// RevisionAudit godoc\n// @Summary revision audit\n// @Description revision audit operation:approve or reject\n// @Tags Revision\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.RevisionAuditReq true \"audit\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/revisions/audit [put]\nfunc (rc *RevisionController) RevisionAudit(ctx *gin.Context) {\n\treq := &schema.RevisionAuditReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAudit,\n\t\tpermission.AnswerAudit,\n\t\tpermission.TagAudit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanReviewQuestion = canList[0]\n\treq.CanReviewAnswer = canList[1]\n\treq.CanReviewTag = canList[2]\n\n\terr = rc.revisionListService.RevisionAudit(ctx, req)\n\thandler.HandleResponse(ctx, err, gin.H{})\n}\n\n// CheckCanUpdateRevision check can update revision\n// @Summary check can update revision\n// @Description check can update revision\n// @Tags Revision\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param id query string true \"id\" default(string)\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/revisions/edit/check [get]\nfunc (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {\n\treq := &schema.CheckCanQuestionUpdate{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\taction := \"\"\n\treq.ID = uid.DeShortID(req.ID)\n\tobjectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)\n\tswitch objectTypeStr {\n\tcase constant.QuestionObjectType:\n\t\taction = permission.QuestionEdit\n\tcase constant.AnswerObjectType:\n\t\taction = permission.AnswerEdit\n\tcase constant.TagObjectType:\n\t\taction = permission.TagEdit\n\tdefault:\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil)\n\t\treturn\n\t}\n\n\tcan, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\tresp, err := rc.revisionListService.CheckCanUpdateRevision(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetReviewingType get reviewing type\n// @Summary get reviewing type\n// @Description get reviewing type\n// @Tags Revision\n// @Produce json\n// @Security ApiKeyAuth\n// @Success 200 {object} handler.RespBody{data=[]schema.GetReviewingTypeResp}\n// @Router /answer/api/v1/reviewing/type [get]\nfunc (rc *RevisionController) GetReviewingType(ctx *gin.Context) {\n\treq := &schema.GetReviewingTypeReq{}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.QuestionAudit,\n\t\tpermission.AnswerAudit,\n\t\tpermission.TagAudit,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanReviewQuestion = canList[0]\n\treq.CanReviewAnswer = canList[1]\n\treq.CanReviewTag = canList[2]\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\n\tresp, err := rc.revisionListService.GetReviewingType(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller/search_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// SearchController tag controller\ntype SearchController struct {\n\tsearchService *content.SearchService\n\tactionService *action.CaptchaService\n}\n\n// NewSearchController new controller\nfunc NewSearchController(\n\tsearchService *content.SearchService,\n\tactionService *action.CaptchaService,\n) *SearchController {\n\treturn &SearchController{\n\t\tsearchService: searchService,\n\t\tactionService: actionService,\n\t}\n}\n\n// Search godoc\n// @Summary search object\n// @Description search object\n// @Tags Search\n// @Produce json\n// @Security ApiKeyAuth\n// @Param q query string true \"query string\"\n// @Param order query string true \"order\" Enums(newest,active,score,relevance)\n// @Success 200 {object} handler.RespBody{data=schema.SearchResp}\n// @Router /answer/api/v1/search [get]\nfunc (sc *SearchController) Search(ctx *gin.Context) {\n\tdto := schema.SearchDTO{}\n\n\tif handler.BindAndCheck(ctx, &dto) {\n\t\treturn\n\t}\n\tdto.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tunit := ctx.ClientIP()\n\tif dto.UserID != \"\" {\n\t\tunit = dto.UserID\n\t}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := sc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionSearch, unit, dto.CaptchaID, dto.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif !isAdmin {\n\t\tsc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionSearch, unit)\n\t}\n\tresp, err := sc.searchService.Search(ctx, &dto)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// SearchDesc get search description\n// @Summary get search description\n// @Description get search description\n// @Tags Search\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SearchResp}\n// @Router /answer/api/v1/search/desc [get]\nfunc (sc *SearchController) SearchDesc(ctx *gin.Context) {\n\tvar finder plugin.Search\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\tfinder = search\n\t\treturn nil\n\t})\n\tresp := &schema.SearchDescResp{}\n\tif finder != nil {\n\t\tresp.Name = finder.Info().Name.Translate(ctx)\n\t\tresp.Icon = finder.Description().Icon\n\t\tresp.Link = finder.Description().Link\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n"
  },
  {
    "path": "internal/controller/siteinfo_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype SiteInfoController struct {\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n}\n\n// NewSiteInfoController new site info controller.\nfunc NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController {\n\treturn &SiteInfoController{\n\t\tsiteInfoService: siteInfoService,\n\t}\n}\n\n// GetSiteInfo get site info\n// @Summary get site info\n// @Description get site info\n// @Tags site\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp}\n// @Router /answer/api/v1/siteinfo [get]\nfunc (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) {\n\tvar err error\n\tresp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision}\n\tresp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.UsersSettings, err = sc.siteInfoService.GetSiteUsersSettings(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.Login, err = sc.siteInfoService.GetSiteLogin(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Questions, err = sc.siteInfoService.GetSiteQuestion(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Tags, err = sc.siteInfoService.GetSiteTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Advanced, err = sc.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tif legal, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil {\n\t\tresp.Legal = &schema.SiteLegalSimpleResp{ExternalContentDisplay: legal.ExternalContentDisplay}\n\t}\n\tif security, err := sc.siteInfoService.GetSiteSecurity(ctx); err == nil {\n\t\tresp.Security = security\n\t}\n\tif aiConf, err := sc.siteInfoService.GetSiteAI(ctx); err == nil {\n\t\tresp.AIEnabled = aiConf.Enabled\n\t}\n\n\tif mcpConf, err := sc.siteInfoService.GetSiteMCP(ctx); err == nil {\n\t\tresp.MCPEnabled = mcpConf.Enabled\n\t}\n\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetSiteLegalInfo get site legal info\n// @Summary get site legal info\n// @Description get site legal info\n// @Tags site\n// @Param info_type query string true \"legal information type\" Enums(tos, privacy)\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}\n// @Router /answer/api/v1/siteinfo/legal [get]\nfunc (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) {\n\treq := &schema.GetSiteLegalInfoReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tsiteLegal, err := sc.siteInfoService.GetSitePolicies(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tresp := &schema.GetSiteLegalInfoResp{}\n\tif req.IsTOS() {\n\t\tresp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText\n\t\tresp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText\n\t} else if req.IsPrivacy() {\n\t\tresp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText\n\t\tresp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetManifestJson get manifest.json\nfunc (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) {\n\tfavicon := \"favicon.ico\"\n\tresp := &schema.GetManifestJsonResp{\n\t\tManifestVersion: 3,\n\t\tVersion:         constant.Version,\n\t\tRevision:        constant.Revision,\n\t\tShortName:       \"Answer\",\n\t\tName:            \"answer.apache.org\",\n\t\tIcons:           schema.CreateManifestJsonIcons(favicon),\n\t\tStartUrl:        \".\",\n\t\tDisplay:         \"standalone\",\n\t\tThemeColor:      \"#000000\",\n\t\tBackgroundColor: \"#ffffff\",\n\t}\n\tbranding, err := sc.siteInfoService.GetSiteBranding(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t} else if len(branding.Favicon) > 0 {\n\t\tresp.Icons = schema.CreateManifestJsonIcons(branding.Favicon)\n\t}\n\tsiteGeneral, err := sc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t} else {\n\t\tresp.Name = siteGeneral.Name\n\t\tresp.ShortName = siteGeneral.Name\n\t}\n\tctx.JSON(http.StatusOK, resp)\n}\n"
  },
  {
    "path": "internal/controller/tag_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/tag\"\n\t\"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// TagController tag controller\ntype TagController struct {\n\ttagService       *tag.TagService\n\ttagCommonService *tag_common.TagCommonService\n\trankService      *rank.RankService\n}\n\n// NewTagController new controller\nfunc NewTagController(\n\ttagService *tag.TagService,\n\ttagCommonService *tag_common.TagCommonService,\n\trankService *rank.RankService,\n) *TagController {\n\treturn &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService}\n}\n\n// SearchTagLike get tag list\n// @Summary get tag list\n// @Description get tag list\n// @Tags Tag\n// @Produce json\n// @Security ApiKeyAuth\n// @Param tag query string false \"tag\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp}\n// @Router /answer/api/v1/question/tags [get]\nfunc (tc *TagController) SearchTagLike(ctx *gin.Context) {\n\treq := &schema.SearchTagLikeReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tresp, err := tc.tagCommonService.SearchTagLike(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetTagsBySlugName get tags list\n// @Summary get tags list\n// @Description get tags list by slug name\n// @Tags Tag\n// @Produce json\n// @Param tags query []string false \"string collection\" collectionFormat(csv)\n// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp}\n// @Router /answer/api/v1/tags [get]\nfunc (tc *TagController) GetTagsBySlugName(ctx *gin.Context) {\n\treq := &schema.SearchTagsBySlugName{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := tc.tagService.GetTagsBySlugName(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// RemoveTag delete tag\n// @Summary delete tag\n// @Description delete tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.RemoveTagReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag [delete]\nfunc (tc *TagController) RemoveTag(ctx *gin.Context) {\n\treq := &schema.RemoveTagReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcan, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\terr = tc.tagService.RemoveTag(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// AddTag add tag\n// @Summary add tag\n// @Description add tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.AddTagReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag [post]\nfunc (tc *TagController) AddTag(ctx *gin.Context) {\n\treq := &schema.AddTagReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.TagAdd,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !canList[0] {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\tresp, err := tc.tagCommonService.AddTag(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateTag update tag\n// @Summary update tag\n// @Description update tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.UpdateTagReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag [put]\nfunc (tc *TagController) UpdateTag(ctx *gin.Context) {\n\treq := &schema.UpdateTagReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.TagEdit,\n\t\tpermission.TagEditWithoutReview,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !canList[0] {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\treq.NoNeedReview = canList[1]\n\n\terr = tc.tagService.UpdateTag(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t} else {\n\t\thandler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview})\n\t}\n}\n\n// RecoverTag recover delete tag\n// @Summary recover delete tag\n// @Description recover delete tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.RecoverTagReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag/recover [post]\nfunc (tc *TagController) RecoverTag(ctx *gin.Context) {\n\treq := &schema.RecoverTagReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcanList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.TagUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !canList[0] {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = tc.tagService.RecoverTag(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetTagInfo get tag one\n// @Summary get tag one\n// @Description get tag one\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param tag_id query string true \"tag id\"\n// @Param tag_name query string true \"tag name\"\n// @Success 200 {object} handler.RespBody{data=schema.GetTagResp}\n// @Router /answer/api/v1/tag [get]\nfunc (tc *TagController) GetTagInfo(ctx *gin.Context) {\n\treq := &schema.GetTagInfoReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcanList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{\n\t\tpermission.TagEdit,\n\t\tpermission.TagDelete,\n\t\tpermission.TagUnDelete,\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = canList[0]\n\treq.CanDelete = canList[1]\n\treq.CanRecover = canList[2]\n\treq.CanMerge = middleware.GetUserIsAdminModerator(ctx)\n\n\tresp, err := tc.tagService.GetTagInfo(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetTagWithPage get tag page\n// @Summary get tag page\n// @Description get tag page\n// @Tags Tag\n// @Produce json\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Param slug_name query string false \"slug_name\"\n// @Param query_cond query string false \"query condition\" Enums(popular, name, newest)\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetTagPageResp}}\n// @Router /answer/api/v1/tags/page [get]\nfunc (tc *TagController) GetTagWithPage(ctx *gin.Context) {\n\treq := &schema.GetTagWithPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := tc.tagService.GetTagWithPage(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif pager.ValPageOutOfRange(resp.Count, req.Page, req.PageSize) {\n\t\thandler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetFollowingTags get following tag list\n// @Summary get following tag list\n// @Description get following tag list\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetFollowingTagsResp}\n// @Router /answer/api/v1/tags/following [get]\nfunc (tc *TagController) GetFollowingTags(ctx *gin.Context) {\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := tc.tagService.GetFollowingTags(ctx, userID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetTagSynonyms get tag synonyms\n// @Summary get tag synonyms\n// @Description get tag synonyms\n// @Tags Tag\n// @Produce json\n// @Param tag_id query int true \"tag id\"\n// @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp}\n// @Router /answer/api/v1/tag/synonyms [get]\nfunc (tc *TagController) GetTagSynonyms(ctx *gin.Context) {\n\treq := &schema.GetTagSynonymsReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcan, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\treq.CanEdit = can\n\n\tresp, err := tc.tagService.GetTagSynonyms(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateTagSynonym update tag\n// @Summary update tag\n// @Description update tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.UpdateTagSynonymReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag/synonym [put]\nfunc (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {\n\treq := &schema.UpdateTagSynonymReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tcan, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, \"\")\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\terr = tc.tagService.UpdateTagSynonym(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// MergeTag merge tag\n// @Summary merge tag\n// @Description merge tag\n// @Security ApiKeyAuth\n// @Tags Tag\n// @Accept json\n// @Produce json\n// @Param data body schema.AddTagReq true \"tag\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/tag/merge [post]\nfunc (tc *TagController) MergeTag(ctx *gin.Context) {\n\treq := &schema.MergeTagReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tisAdminModerator := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdminModerator {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\terr := tc.tagService.MergeTag(ctx, req)\n\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/template_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/apache/answer/plugin\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\ttemplaterender \"github.com/apache/answer/internal/controller/template_render\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/ui\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nvar SiteUrl = \"\"\n\ntype TemplateController struct {\n\tscriptPath               []string\n\tcssPath                  string\n\ttemplateRenderController *templaterender.TemplateRenderController\n\tsiteInfoService          siteinfo_common.SiteInfoCommonService\n\teventQueueService        eventqueue.Service\n\tuserService              *content.UserService\n\tquestionService          *content.QuestionService\n}\n\n// NewTemplateController new controller\nfunc NewTemplateController(\n\ttemplateRenderController *templaterender.TemplateRenderController,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\teventQueueService eventqueue.Service,\n\tuserService *content.UserService,\n\tquestionService *content.QuestionService,\n) *TemplateController {\n\tscript, css := GetStyle()\n\treturn &TemplateController{\n\t\tscriptPath:               script,\n\t\tcssPath:                  css,\n\t\ttemplateRenderController: templateRenderController,\n\t\tsiteInfoService:          siteInfoService,\n\t\teventQueueService:        eventQueueService,\n\t\tuserService:              userService,\n\t\tquestionService:          questionService,\n\t}\n}\nfunc GetStyle() (script []string, css string) {\n\tfile, err := ui.Build.ReadFile(\"build/index.html\")\n\tif err != nil {\n\t\treturn\n\t}\n\tscriptRegexp := regexp.MustCompile(`<script defer=\"defer\" src=\"([^\"]*)\"></script>`)\n\tscriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1)\n\tfor _, s := range scriptData {\n\t\tif len(s) == 2 {\n\t\t\tscript = append(script, s[1])\n\t\t}\n\t}\n\n\tcssRegexp := regexp.MustCompile(`<link href=\"(.*)\" rel=\"stylesheet\">`)\n\tcssListData := cssRegexp.FindStringSubmatch(string(file))\n\tif len(cssListData) == 2 {\n\t\tcss = cssListData[1]\n\t}\n\treturn\n}\nfunc (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp {\n\tvar err error\n\tresp := &schema.TemplateSiteInfoResp{}\n\tresp.General, err = tc.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tSiteUrl = resp.General.SiteUrl\n\tresp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Year = fmt.Sprintf(\"%d\", time.Now().Year())\n\treturn resp\n}\n\n// Index question list\nfunc (tc *TemplateController) Index(ctx *gin.Context) {\n\treq := &schema.QuestionPageReq{\n\t\tOrderCond: \"newest\",\n\t}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tvar page = req.Page\n\n\tdata, count, err := tc.templateRenderController.Index(ctx, req)\n\tif err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\thotQuestionReq := &schema.QuestionPageReq{\n\t\tPage:      1,\n\t\tPageSize:  6,\n\t\tOrderCond: \"hot\",\n\t\tInDays:    7,\n\t}\n\thotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tsiteInfo.Canonical = siteInfo.General.SiteUrl\n\n\tUrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\tsiteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID\n\n\tsiteInfo.Title = \"\"\n\ttc.html(ctx, http.StatusOK, \"question.html\", siteInfo, gin.H{\n\t\t\"data\":        data,\n\t\t\"useTitle\":    UrlUseTitle,\n\t\t\"page\":        templaterender.Paginator(page, req.PageSize, count),\n\t\t\"path\":        \"questions\",\n\t\t\"hotQuestion\": hotQuestion,\n\t})\n}\n\nfunc (tc *TemplateController) QuestionList(ctx *gin.Context) {\n\treq := &schema.QuestionPageReq{\n\t\tOrderCond: \"newest\",\n\t}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tvar page = req.Page\n\tdata, count, err := tc.templateRenderController.Index(ctx, req)\n\tif err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\thotQuestionReq := &schema.QuestionPageReq{\n\t\tPage:      1,\n\t\tPageSize:  6,\n\t\tOrderCond: \"hot\",\n\t\tInDays:    7,\n\t}\n\thotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tsiteInfo.Canonical = fmt.Sprintf(\"%s/questions\", siteInfo.General.SiteUrl)\n\tif page > 1 {\n\t\tsiteInfo.Canonical = fmt.Sprintf(\"%s/questions?page=%d\", siteInfo.General.SiteUrl, page)\n\t}\n\n\tUrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\tsiteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID\n\n\tsiteInfo.Title = fmt.Sprintf(\"%s - %s\", translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name)\n\ttc.html(ctx, http.StatusOK, \"question.html\", siteInfo, gin.H{\n\t\t\"data\":        data,\n\t\t\"useTitle\":    UrlUseTitle,\n\t\t\"page\":        templaterender.Paginator(page, req.PageSize, count),\n\t\t\"hotQuestion\": hotQuestion,\n\t})\n}\n\nfunc (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {\n\tquestionID := ctx.Param(\"id\")\n\ttitle := ctx.Param(\"title\")\n\tanswerID := uid.DeShortID(title)\n\ttitleIsAnswerID := false\n\tneedChangeShortID := false\n\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(answerID)\n\tif err == nil && objectType == constant.AnswerObjectType {\n\t\ttitleIsAnswerID = true\n\t}\n\n\tsiteSeo, err := tc.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\tisShortID := uid.IsShortID(questionID)\n\tif siteSeo.IsShortLink() {\n\t\tif !isShortID {\n\t\t\tquestionID = uid.EnShortID(questionID)\n\t\t\tneedChangeShortID = true\n\t\t}\n\t\tif titleIsAnswerID {\n\t\t\tanswerID = uid.EnShortID(answerID)\n\t\t}\n\t} else {\n\t\tif isShortID {\n\t\t\tneedChangeShortID = true\n\t\t\tquestionID = uid.DeShortID(questionID)\n\t\t}\n\t\tif titleIsAnswerID {\n\t\t\tanswerID = uid.DeShortID(answerID)\n\t\t}\n\t}\n\n\tif _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil {\n\t\tanswerID = \"\"\n\t\ttitleIsAnswerID = false\n\t}\n\n\turl = fmt.Sprintf(\"%s/questions/%s\", siteInfo.General.SiteUrl, questionID)\n\tif siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {\n\t\tif len(ctx.Request.URL.Query()) > 0 {\n\t\t\turl = fmt.Sprintf(\"%s?%s\", url, ctx.Request.URL.RawQuery)\n\t\t}\n\t\tif needChangeShortID {\n\t\t\treturn true, url\n\t\t}\n\t\t// not have title\n\t\tif titleIsAnswerID || len(title) == 0 {\n\t\t\treturn false, \"\"\n\t\t}\n\n\t\treturn true, url\n\t} else {\n\t\tdetail, err := tc.templateRenderController.QuestionDetail(ctx, questionID)\n\t\tif err != nil {\n\t\t\ttc.Page404(ctx)\n\t\t\treturn\n\t\t}\n\t\turl = fmt.Sprintf(\"%s/%s\", url, htmltext.UrlTitle(detail.Title))\n\t\tif titleIsAnswerID {\n\t\t\turl = fmt.Sprintf(\"%s/%s\", url, answerID)\n\t\t}\n\n\t\tif len(ctx.Request.URL.Query()) > 0 {\n\t\t\turl = fmt.Sprintf(\"%s?%s\", url, ctx.Request.URL.RawQuery)\n\t\t}\n\t\t// have title\n\t\tif len(title) > 0 && !titleIsAnswerID && correctTitle {\n\t\t\tif needChangeShortID {\n\t\t\t\treturn true, url\n\t\t\t}\n\t\t\treturn false, \"\"\n\t\t}\n\t\treturn true, url\n\t}\n}\n\n// QuestionInfo question and answers info\nfunc (tc *TemplateController) QuestionInfo(ctx *gin.Context) {\n\tid := ctx.Param(\"id\")\n\ttitle := ctx.Param(\"title\")\n\tanswerid := ctx.Param(\"answerid\")\n\tshareUsername := ctx.Query(\"share\")\n\tif checker.IsQuestionsIgnorePath(id) {\n\t\t// if id == \"ask\" {\n\t\tfile, err := ui.Build.ReadFile(\"build/index.html\")\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\ttc.Page404(ctx)\n\t\t\treturn\n\t\t}\n\t\tctx.Header(\"content-type\", \"text/html;charset=utf-8\")\n\t\tctx.String(http.StatusOK, string(file))\n\t\treturn\n\t}\n\n\tcorrectTitle := false\n\n\tdetail, err := tc.templateRenderController.QuestionDetail(ctx, id)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tif len(shareUsername) > 0 {\n\t\tuserInfo, err := tc.userService.GetOtherUserInfoByUsername(\n\t\t\tctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername})\n\t\tif err == nil {\n\t\t\ttc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID).\n\t\t\t\tQID(id, detail.UserID).AID(answerid, \"\"))\n\t\t}\n\t}\n\tencodeTitle := htmltext.UrlTitle(detail.Title)\n\tif encodeTitle == title {\n\t\tcorrectTitle = true\n\t}\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tjump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle)\n\tif jump {\n\t\tctx.Redirect(http.StatusFound, jumpurl)\n\t\treturn\n\t}\n\n\t// answers\n\tanswerReq := &schema.AnswerListReq{\n\t\tQuestionID: id,\n\t\tOrder:      \"\",\n\t\tPage:       1,\n\t\tPageSize:   999,\n\t\tUserID:     \"\",\n\t}\n\tanswers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\t// comments\n\tobjectIDs := []string{uid.DeShortID(id)}\n\tfor _, answer := range answers {\n\t\tanswerID := uid.DeShortID(answer.ID)\n\t\tobjectIDs = append(objectIDs, answerID)\n\t}\n\tcomments, err := tc.templateRenderController.CommentList(ctx, objectIDs)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\tUrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\tsiteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID\n\n\t// related question\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\trelatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID)\n\n\tsiteInfo.Canonical = fmt.Sprintf(\"%s/questions/%s/%s\", siteInfo.General.SiteUrl, id, encodeTitle)\n\tif siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {\n\t\tsiteInfo.Canonical = fmt.Sprintf(\"%s/questions/%s\", siteInfo.General.SiteUrl, id)\n\t}\n\tjsonLD := &schema.QAPageJsonLD{}\n\tjsonLD.Context = \"https://schema.org\"\n\tjsonLD.Type = \"QAPage\"\n\tjsonLD.MainEntity.Type = \"Question\"\n\tjsonLD.MainEntity.Name = detail.Title\n\tjsonLD.MainEntity.Text = detail.HTML\n\tjsonLD.MainEntity.AnswerCount = int(answerCount)\n\tjsonLD.MainEntity.UpvoteCount = detail.VoteCount\n\tjsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0)\n\tjsonLD.MainEntity.Author.Type = \"Person\"\n\tjsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName\n\tjsonLD.MainEntity.Author.URL = fmt.Sprintf(\"%s/users/%s\", siteInfo.General.SiteUrl, detail.UserInfo.Username)\n\tanswerList := make([]*schema.SuggestedAnswerItem, 0)\n\tfor _, answer := range answers {\n\t\tif answer.Accepted == schema.AnswerAcceptedEnable {\n\t\t\tacceptedAnswerItem := &schema.AcceptedAnswerItem{}\n\t\t\tacceptedAnswerItem.Type = \"Answer\"\n\t\t\tacceptedAnswerItem.Text = answer.HTML\n\t\t\tacceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0)\n\t\t\tacceptedAnswerItem.UpvoteCount = answer.VoteCount\n\t\t\tacceptedAnswerItem.URL = fmt.Sprintf(\"%s/%s\", siteInfo.Canonical, answer.ID)\n\t\t\tacceptedAnswerItem.Author.Type = \"Person\"\n\t\t\tacceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName\n\t\t\tacceptedAnswerItem.Author.URL = fmt.Sprintf(\"%s/users/%s\", siteInfo.General.SiteUrl, answer.UserInfo.Username)\n\t\t\tjsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem\n\t\t} else {\n\t\t\titem := &schema.SuggestedAnswerItem{}\n\t\t\titem.Type = \"Answer\"\n\t\t\titem.Text = answer.HTML\n\t\t\titem.DateCreated = time.Unix(answer.CreateTime, 0)\n\t\t\titem.UpvoteCount = answer.VoteCount\n\t\t\titem.URL = fmt.Sprintf(\"%s/%s\", siteInfo.Canonical, answer.ID)\n\t\t\titem.Author.Type = \"Person\"\n\t\t\titem.Author.Name = answer.UserInfo.DisplayName\n\t\t\titem.Author.URL = fmt.Sprintf(\"%s/users/%s\", siteInfo.General.SiteUrl, answer.UserInfo.Username)\n\t\t\tanswerList = append(answerList, item)\n\t\t}\n\t}\n\tjsonLD.MainEntity.SuggestedAnswer = answerList\n\tjsonLDStr, err := json.Marshal(jsonLD)\n\tif err == nil {\n\t\tsiteInfo.JsonLD = `<script data-react-helmet=\"true\" type=\"application/ld+json\">` + string(jsonLDStr) + ` </script>`\n\t}\n\n\tsiteInfo.Description = htmltext.FetchExcerpt(detail.HTML, \"...\", 240)\n\ttags := make([]string, 0)\n\tfor _, tag := range detail.Tags {\n\t\ttags = append(tags, tag.DisplayName)\n\t}\n\tsiteInfo.Keywords = strings.ReplaceAll(strings.Trim(fmt.Sprint(tags), \"[]\"), \" \", \",\")\n\tsiteInfo.Title = fmt.Sprintf(\"%s - %s\", detail.Title, siteInfo.General.Name)\n\ttc.html(ctx, http.StatusOK, \"question-detail.html\", siteInfo, gin.H{\n\t\t\"id\":              id,\n\t\t\"answerid\":        answerid,\n\t\t\"detail\":          detail,\n\t\t\"answers\":         answers,\n\t\t\"comments\":        comments,\n\t\t\"noindex\":         detail.Show == entity.QuestionHide,\n\t\t\"useTitle\":        UrlUseTitle,\n\t\t\"relatedQuestion\": relatedQuestion,\n\t})\n}\n\n// TagList tags list\nfunc (tc *TemplateController) TagList(ctx *gin.Context) {\n\treq := &schema.GetTagWithPageReq{\n\t\tPageSize: constant.DefaultPageSize,\n\t\tPage:     1,\n\t}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tdata, err := tc.templateRenderController.TagList(ctx, req)\n\tif err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tpage := templaterender.Paginator(req.Page, req.PageSize, data.Count)\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tsiteInfo.Canonical = fmt.Sprintf(\"%s/tags\", siteInfo.General.SiteUrl)\n\tif req.Page > 1 {\n\t\tsiteInfo.Canonical = fmt.Sprintf(\"%s/tags?page=%d\", siteInfo.General.SiteUrl, req.Page)\n\t}\n\tsiteInfo.Title = fmt.Sprintf(\"%s - %s\", translator.Tr(handler.GetLangByCtx(ctx), constant.TagsListTitleTrKey), siteInfo.General.Name)\n\ttc.html(ctx, http.StatusOK, \"tags.html\", siteInfo, gin.H{\n\t\t\"page\": page,\n\t\t\"data\": data,\n\t})\n}\n\n// TagInfo taginfo\nfunc (tc *TemplateController) TagInfo(ctx *gin.Context) {\n\ttag := ctx.Param(\"tag\")\n\treq := &schema.GetTamplateTagInfoReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tnowPage := req.Page\n\treq.Name = tag\n\ttagInfo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tpage := templaterender.Paginator(nowPage, req.PageSize, questionCount)\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tsiteInfo.Canonical = fmt.Sprintf(\"%s/tags/%s\", siteInfo.General.SiteUrl, tag)\n\tif req.Page > 1 {\n\t\tsiteInfo.Canonical = fmt.Sprintf(\"%s/tags/%s?page=%d\", siteInfo.General.SiteUrl, tag, req.Page)\n\t}\n\tsiteInfo.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, \"...\", 240)\n\tif len(tagInfo.ParsedText) == 0 {\n\t\tsiteInfo.Description = translator.Tr(handler.GetLangByCtx(ctx), constant.TagHasNoDescription)\n\t}\n\tsiteInfo.Keywords = tagInfo.DisplayName\n\n\tUrlUseTitle := siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\tsiteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID\n\n\tsiteInfo.Title = fmt.Sprintf(\"'%s' %s - %s\", tagInfo.DisplayName, translator.Tr(handler.GetLangByCtx(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name)\n\ttc.html(ctx, http.StatusOK, \"tag-detail.html\", siteInfo, gin.H{\n\t\t\"tag\":           tagInfo,\n\t\t\"questionList\":  questionList,\n\t\t\"questionCount\": questionCount,\n\t\t\"useTitle\":      UrlUseTitle,\n\t\t\"page\":          page,\n\t})\n}\n\n// UserInfo user info\nfunc (tc *TemplateController) UserInfo(ctx *gin.Context) {\n\tusername := ctx.Param(\"username\")\n\tif username == \"\" {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\texist := checker.IsUsersIgnorePath(username)\n\tif exist {\n\t\tfile, err := ui.Build.ReadFile(\"build/index.html\")\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\ttc.Page404(ctx)\n\t\t\treturn\n\t\t}\n\t\tctx.Header(\"content-type\", \"text/html;charset=utf-8\")\n\t\tctx.String(http.StatusOK, string(file))\n\t\treturn\n\t}\n\treq := &schema.GetOtherUserInfoByUsernameReq{}\n\treq.Username = username\n\tuserinfo, err := tc.templateRenderController.UserInfo(ctx, req)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\tquestionList, answerList, err := tc.questionService.SearchUserTopList(ctx, req.Username, \"\")\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\n\tsiteInfo := tc.SiteInfo(ctx)\n\tsiteInfo.Canonical = fmt.Sprintf(\"%s/users/%s\", siteInfo.General.SiteUrl, username)\n\tsiteInfo.Title = fmt.Sprintf(\"%s - %s\", username, siteInfo.General.Name)\n\ttc.html(ctx, http.StatusOK, \"homepage.html\", siteInfo, gin.H{\n\t\t\"userinfo\":     userinfo,\n\t\t\"bio\":          template.HTML(userinfo.BioHTML),\n\t\t\"topQuestions\": questionList,\n\t\t\"topAnswers\":   answerList,\n\t})\n}\n\nfunc (tc *TemplateController) Page404(ctx *gin.Context) {\n\ttc.html(ctx, http.StatusNotFound, \"404.html\", tc.SiteInfo(ctx), gin.H{})\n}\n\nfunc (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) {\n\tprefix := \"\"\n\tcssPath := \"\"\n\tscriptPath := make([]string, len(tc.scriptPath))\n\n\t_ = plugin.CallCDN(func(fn plugin.CDN) error {\n\t\tprefix = fn.GetStaticPrefix()\n\t\treturn nil\n\t})\n\n\tif prefix != \"\" {\n\t\tif prefix[len(prefix)-1:] == \"/\" {\n\t\t\tprefix = strings.TrimSuffix(prefix, \"/\")\n\t\t}\n\t\tcssPath = prefix + tc.cssPath\n\t\tfor i, path := range tc.scriptPath {\n\t\t\tscriptPath[i] = prefix + path\n\t\t}\n\t} else {\n\t\tcssPath = tc.cssPath\n\t\tscriptPath = tc.scriptPath\n\t}\n\n\tdata[\"siteinfo\"] = siteInfo\n\tdata[\"baseURL\"] = \"\"\n\tif parsedUrl, err := url.Parse(siteInfo.General.SiteUrl); err == nil {\n\t\tdata[\"baseURL\"] = parsedUrl.Path\n\t}\n\tdata[\"scriptPath\"] = scriptPath\n\tdata[\"cssPath\"] = cssPath\n\tdata[\"keywords\"] = siteInfo.Keywords\n\tif siteInfo.Description == \"\" {\n\t\tsiteInfo.Description = siteInfo.General.Description\n\t}\n\tdata[\"title\"] = siteInfo.Title\n\tif siteInfo.Title == \"\" {\n\t\tdata[\"title\"] = siteInfo.General.Name\n\t}\n\tdata[\"description\"] = siteInfo.Description\n\tdata[\"language\"] = handler.GetLangByCtx(ctx)\n\tdata[\"timezone\"] = siteInfo.Interface.TimeZone\n\tlanguage := strings.ReplaceAll(siteInfo.Interface.Language, \"_\", \"-\")\n\tdata[\"lang\"] = language\n\tdata[\"HeadCode\"] = siteInfo.CustomCssHtml.CustomHead\n\tdata[\"HeaderCode\"] = siteInfo.CustomCssHtml.CustomHeader\n\tdata[\"FooterCode\"] = siteInfo.CustomCssHtml.CustomFooter\n\tdata[\"Version\"] = constant.Version\n\tdata[\"Revision\"] = constant.Revision\n\t_, ok := data[\"path\"]\n\tif !ok {\n\t\tdata[\"path\"] = \"\"\n\t}\n\tctx.Header(\"X-Frame-Options\", \"DENY\")\n\tctx.HTML(code, tpl, data)\n}\n\nfunc (tc *TemplateController) OpenSearch(ctx *gin.Context) {\n\tif tc.checkPrivateMode(ctx) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\ttc.templateRenderController.OpenSearch(ctx)\n}\n\nfunc (tc *TemplateController) Sitemap(ctx *gin.Context) {\n\tif tc.checkPrivateMode(ctx) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\ttc.templateRenderController.Sitemap(ctx)\n}\n\nfunc (tc *TemplateController) SitemapPage(ctx *gin.Context) {\n\tif tc.checkPrivateMode(ctx) {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tpage := 0\n\tpageParam := ctx.Param(\"page\")\n\tpageRegexp := regexp.MustCompile(`question-(.*).xml`)\n\tpageStr := pageRegexp.FindStringSubmatch(pageParam)\n\tif len(pageStr) != 2 {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\tpage = converter.StringToInt(pageStr[1])\n\tif page == 0 {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n\terr := tc.templateRenderController.SitemapPage(ctx, page)\n\tif err != nil {\n\t\ttc.Page404(ctx)\n\t\treturn\n\t}\n}\n\nfunc (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool {\n\tresp, err := tc.siteInfoService.GetSiteSecurity(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false\n\t}\n\tif resp.LoginRequired {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/controller/template_render/answer.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n)\n\nfunc (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {\n\treturn t.answerService.SearchList(ctx, req)\n}\n\nfunc (t *TemplateRenderController) AnswerDetail(ctx context.Context, id string) (*schema.AnswerInfo, error) {\n\treturn t.answerService.GetDetail(ctx, id)\n}\n"
  },
  {
    "path": "internal/controller/template_render/comment.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\nfunc (t *TemplateRenderController) CommentList(\n\tctx context.Context,\n\tobjectIDs []string,\n) (\n\tcomments map[string][]*schema.GetCommentResp,\n\terr error,\n) {\n\tcomments = make(map[string][]*schema.GetCommentResp, len(objectIDs))\n\n\tfor _, objectID := range objectIDs {\n\t\tvar (\n\t\t\treq = &schema.GetCommentWithPageReq{\n\t\t\t\tPage:      1,\n\t\t\t\tPageSize:  3,\n\t\t\t\tObjectID:  objectID,\n\t\t\t\tQueryCond: \"vote\",\n\t\t\t\tUserID:    \"\",\n\t\t\t}\n\t\t\tpageModel *pager.PageModel\n\t\t)\n\t\tpageModel, err = t.commentService.GetCommentWithPage(ctx, req)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tli := pageModel.List\n\t\tcomments[objectID] = li.([]*schema.GetCommentResp)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/controller/template_render/controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"math\"\n\n\t\"github.com/apache/answer/internal/service/content\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/google/wire\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/tag\"\n)\n\n// ProviderSetTemplateRenderController is template render controller providers.\nvar ProviderSetTemplateRenderController = wire.NewSet(\n\tNewTemplateRenderController,\n)\n\ntype TemplateRenderController struct {\n\tquestionService *content.QuestionService\n\tuserService     *content.UserService\n\ttagService      *tag.TagService\n\tanswerService   *content.AnswerService\n\tcommentService  *comment.CommentService\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n\tquestionRepo    questioncommon.QuestionRepo\n}\n\nfunc NewTemplateRenderController(\n\tquestionService *content.QuestionService,\n\tuserService *content.UserService,\n\ttagService *tag.TagService,\n\tanswerService *content.AnswerService,\n\tcommentService *comment.CommentService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tquestionRepo questioncommon.QuestionRepo,\n) *TemplateRenderController {\n\treturn &TemplateRenderController{\n\t\tquestionService: questionService,\n\t\tuserService:     userService,\n\t\ttagService:      tagService,\n\t\tanswerService:   answerService,\n\t\tcommentService:  commentService,\n\t\tquestionRepo:    questionRepo,\n\t\tsiteInfoService: siteInfoService,\n\t}\n}\n\n// Paginator page\n// page : now page\n// pageSize : Number per page\n// nums : Total\n// Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned\nfunc Paginator(page, pageSize int, nums int64) *schema.Paginator {\n\tif pageSize == 0 {\n\t\tpageSize = 10\n\t}\n\n\tvar prevpage int // Previous page address\n\tvar nextpage int // Address on the last page\n\t// Generate the total number of pages based on the total number of nums and the number of prepage pages\n\ttotalpages := int(math.Ceil(float64(nums) / float64(pageSize))) // Total number of Pages\n\tif page > totalpages {\n\t\tpage = totalpages\n\t}\n\tif page <= 0 {\n\t\tpage = 1\n\t}\n\tvar pages []int\n\tswitch {\n\tcase page >= totalpages-5 && totalpages > 5: // The last 5 pages\n\t\tstart := totalpages - 5 + 1\n\t\tprevpage = page - 1\n\t\tnextpage = int(math.Min(float64(totalpages), float64(page+1)))\n\t\tpages = make([]int, 5)\n\t\tfor i := range pages {\n\t\t\tpages[i] = start + i\n\t\t}\n\tcase page >= 3 && totalpages > 5:\n\t\tstart := page - 3 + 1\n\t\tpages = make([]int, 5)\n\t\tfor i := range pages {\n\t\t\tpages[i] = start + i\n\t\t}\n\t\tprevpage = page - 1\n\t\tnextpage = page + 1\n\tdefault:\n\t\tpages = make([]int, int(math.Min(5, float64(totalpages))))\n\t\tfor i := range pages {\n\t\t\tpages[i] = i + 1\n\t\t}\n\t\tprevpage = int(math.Max(float64(1), float64(page-1)))\n\t\tnextpage = page + 1\n\t}\n\tpaginator := &schema.Paginator{}\n\tpaginator.Pages = pages\n\tpaginator.Totalpages = totalpages\n\tpaginator.Prevpage = prevpage\n\tpaginator.Nextpage = nextpage\n\tpaginator.Currpage = page\n\treturn paginator\n}\n"
  },
  {
    "path": "internal/controller/template_render/question.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"html/template\"\n\t\"math\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) {\n\treturn t.questionService.GetQuestionPage(ctx, req)\n}\n\nfunc (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfoResp, err error) {\n\treturn t.questionService.GetQuestion(ctx, id, \"\", schema.QuestionPermission{})\n}\n\nfunc (t *TemplateRenderController) Sitemap(ctx *gin.Context) {\n\tgeneral, err := t.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(\"get site general failed:\", err)\n\t\treturn\n\t}\n\tsiteInfo, err := t.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(\"get site GetSiteSeo failed:\", err)\n\t\treturn\n\t}\n\n\tquestions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize)\n\tif err != nil {\n\t\tlog.Errorf(\"get sitemap questions failed: %s\", err)\n\t\treturn\n\t}\n\n\tctx.Header(\"Content-Type\", \"application/xml\")\n\tif len(questions) < constant.SitemapMaxSize {\n\t\tctx.HTML(\n\t\t\thttp.StatusOK, \"sitemap.xml\", gin.H{\n\t\t\t\t\"xmlHeader\": template.HTML(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`),\n\t\t\t\t\"list\":      questions,\n\t\t\t\t\"general\":   general,\n\t\t\t\t\"hastitle\": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\t\t\t\tsiteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID,\n\t\t\t},\n\t\t)\n\t\treturn\n\t}\n\n\tquestionNum, err := t.questionRepo.GetQuestionCount(ctx)\n\tif err != nil {\n\t\tlog.Error(\"GetQuestionCount error\", err)\n\t\treturn\n\t}\n\tvar pageList []int\n\ttotalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))\n\tfor i := 1; i <= totalPages; i++ {\n\t\tpageList = append(pageList, i)\n\t}\n\tctx.HTML(\n\t\thttp.StatusOK, \"sitemap-list.xml\", gin.H{\n\t\t\t\"xmlHeader\": template.HTML(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`),\n\t\t\t\"page\":      pageList,\n\t\t\t\"general\":   general,\n\t\t},\n\t)\n}\n\nfunc (t *TemplateRenderController) OpenSearch(ctx *gin.Context) {\n\tgeneral, err := t.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(\"get site general failed:\", err)\n\t\treturn\n\t}\n\n\tfavicon := general.SiteUrl + \"/favicon.ico\"\n\tbranding, err := t.siteInfoService.GetSiteBranding(ctx)\n\tif err == nil && len(branding.Favicon) > 0 {\n\t\tfavicon = branding.Favicon\n\t}\n\n\tctx.Header(\"Content-Type\", \"application/xml\")\n\tctx.HTML(\n\t\thttp.StatusOK, \"opensearch.xml\", gin.H{\n\t\t\t\"general\": general,\n\t\t\t\"favicon\": favicon,\n\t\t},\n\t)\n}\n\nfunc (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {\n\tgeneral, err := t.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Error(\"get site general failed:\", err)\n\t\treturn err\n\t}\n\tsiteInfo, err := t.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(\"get site GetSiteSeo failed:\", err)\n\t\treturn err\n\t}\n\n\tquestions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize)\n\tif err != nil {\n\t\tlog.Errorf(\"get sitemap questions failed: %s\", err)\n\t\treturn err\n\t}\n\tctx.Header(\"Content-Type\", \"application/xml\")\n\tctx.HTML(\n\t\thttp.StatusOK, \"sitemap.xml\", gin.H{\n\t\t\t\"xmlHeader\": template.HTML(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`),\n\t\t\t\"list\":      questions,\n\t\t\t\"general\":   general,\n\t\t\t\"hastitle\": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\t\t\tsiteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID,\n\t\t},\n\t)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/controller/template_render/tags.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/jinzhu/copier\"\n\t\"golang.org/x/net/context\"\n)\n\nfunc (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) {\n\tresp, err = q.tagService.GetTagWithPage(ctx, req)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) {\n\tdto := &schema.GetTagInfoReq{}\n\t_ = copier.Copy(dto, req)\n\tresp, err = q.tagService.GetTagInfo(ctx, dto)\n\tif err != nil {\n\t\treturn\n\t}\n\tsearchQuestion := &schema.QuestionPageReq{}\n\tsearchQuestion.Page = req.Page\n\tsearchQuestion.PageSize = req.PageSize\n\tsearchQuestion.OrderCond = \"newest\"\n\tsearchQuestion.Tag = req.Name\n\tsearchQuestion.LoginUserID = req.UserID\n\tquestionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn resp, questionList, questionCount, err\n}\n"
  },
  {
    "path": "internal/controller/template_render/userinfo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage templaterender\n\nimport (\n\t\"github.com/apache/answer/internal/schema\"\n\t\"golang.org/x/net/context\"\n)\n\nfunc (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) {\n\treturn q.userService.GetOtherUserInfoByUsername(ctx, req)\n}\n"
  },
  {
    "path": "internal/controller/upload_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/uploader\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\nconst (\n\t// file is uploaded by markdown(or something else) editor\n\tfileFromPost = \"post\"\n\t// file is used to upload the post attachment\n\tfileFromPostAttachment = \"post_attachment\"\n\t// file is used to change the user's avatar\n\tfileFromAvatar = \"avatar\"\n\t// file is logo/icon images\n\tfileFromBranding = \"branding\"\n)\n\n// UploadController upload controller\ntype UploadController struct {\n\tuploaderService uploader.UploaderService\n}\n\n// NewUploadController new controller\nfunc NewUploadController(uploaderService uploader.UploaderService) *UploadController {\n\treturn &UploadController{\n\t\tuploaderService: uploaderService,\n\t}\n}\n\n// UploadFile upload file\n// @Summary upload file\n// @Description upload file\n// @Tags Upload\n// @Accept multipart/form-data\n// @Security ApiKeyAuth\n// @Param source formData string true \"identify the source of the file upload\" Enums(post, post_attachment, avatar, branding)\n// @Param file formData file true \"file\"\n// @Success 200 {object} handler.RespBody{data=string}\n// @Router /answer/api/v1/file [post]\nfunc (uc *UploadController) UploadFile(ctx *gin.Context) {\n\tvar (\n\t\turl string\n\t\terr error\n\t)\n\n\tsource := ctx.PostForm(\"source\")\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tswitch source {\n\tcase fileFromAvatar:\n\t\turl, err = uc.uploaderService.UploadAvatarFile(ctx, userID)\n\tcase fileFromPost:\n\t\turl, err = uc.uploaderService.UploadPostFile(ctx, userID)\n\tcase fileFromBranding:\n\t\tif !middleware.GetIsAdminFromContext(ctx) {\n\t\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\t\treturn\n\t\t}\n\t\turl, err = uc.uploaderService.UploadBrandingFile(ctx, userID)\n\tcase fileFromPostAttachment:\n\t\turl, err = uc.uploaderService.UploadPostAttachment(ctx, userID)\n\tdefault:\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)\n\t\treturn\n\t}\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, err, url)\n}\n\n// PostRender render post content\n// @Summary render post content\n// @Description render post content\n// @Tags Upload\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.PostRenderReq true \"PostRenderReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/post/render [post]\nfunc (uc *UploadController) PostRender(ctx *gin.Context) {\n\treq := &schema.PostRenderReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content))\n}\n"
  },
  {
    "path": "internal/controller/user_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// UserController user controller\ntype UserController struct {\n\tuserService                   *content.UserService\n\tauthService                   *auth.AuthService\n\tactionService                 *action.CaptchaService\n\temailService                  *export.EmailService\n\tsiteInfoCommonService         siteinfo_common.SiteInfoCommonService\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService\n}\n\n// NewUserController new controller\nfunc NewUserController(\n\tauthService *auth.AuthService,\n\tuserService *content.UserService,\n\tactionService *action.CaptchaService,\n\temailService *export.EmailService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService,\n) *UserController {\n\treturn &UserController{\n\t\tauthService:                   authService,\n\t\tuserService:                   userService,\n\t\tactionService:                 actionService,\n\t\temailService:                  emailService,\n\t\tsiteInfoCommonService:         siteInfoCommonService,\n\t\tuserNotificationConfigService: userNotificationConfigService,\n\t}\n}\n\n// GetUserInfoByUserID get user info, if user no login response http code is 200, but user info is null\n// @Summary GetUserInfoByUserID\n// @Description get user info, if user no login response http code is 200, but user info is null\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Success 200 {object} handler.RespBody{data=schema.GetCurrentLoginUserInfoResp}\n// @Router /answer/api/v1/user/info [get]\nfunc (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) {\n\ttoken := middleware.ExtractToken(ctx)\n\tif len(token) == 0 {\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\n\t// if user is no login return null in data\n\tuserInfo, _ := uc.authService.GetUserCacheInfo(ctx, token)\n\tif userInfo == nil {\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID)\n\tuc.setVisitCookies(ctx, userInfo.VisitToken, false)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetOtherUserInfoByUsername godoc\n// @Summary GetOtherUserInfoByUsername\n// @Description GetOtherUserInfoByUsername\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param username query string true \"username\"\n// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}\n// @Router /answer/api/v1/personal/user/info [get]\nfunc (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {\n\treq := &schema.GetOtherUserInfoByUsernameReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\n\tresp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserEmailLogin godoc\n// @Summary UserEmailLogin\n// @Description UserEmailLogin\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param data body schema.UserEmailLoginReq true \"UserEmailLogin\"\n// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}\n// @Router /answer/api/v1/user/login/email [post]\nfunc (uc *UserController) UserEmailLogin(ctx *gin.Context) {\n\treq := &schema.UserEmailLoginReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresp, err := uc.userService.EmailLogin(ctx, req)\n\tif err != nil {\n\t\tuc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, ctx.ClientIP())\n\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\tErrorField: \"e_mail\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.EmailOrPasswordWrong),\n\t\t})\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)\n\t\treturn\n\t}\n\tif !isAdmin {\n\t\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP())\n\t}\n\tif resp.Status == constant.UserSuspended {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),\n\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})\n\t\treturn\n\t}\n\tuc.setVisitCookies(ctx, resp.VisitToken, true)\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// RetrievePassWord godoc\n// @Summary RetrievePassWord\n// @Description RetrievePassWord\n// @Tags User\n// @Accept  json\n// @Produce  json\n// @Param data body schema.UserRetrievePassWordRequest  true \"UserRetrievePassWordRequest\"\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/user/password/reset [post]\nfunc (uc *UserController) RetrievePassWord(ctx *gin.Context) {\n\treq := &schema.UserRetrievePassWordRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\terr := uc.userService.RetrievePassWord(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UseRePassWord godoc\n// @Summary UseRePassWord\n// @Description UseRePassWord\n// @Tags User\n// @Accept  json\n// @Produce  json\n// @Param data body schema.UserRePassWordRequest  true \"UserRePassWordRequest\"\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/user/password/replacement [post]\nfunc (uc *UserController) UseRePassWord(ctx *gin.Context) {\n\treq := &schema.UserRePassWordRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)\n\tif len(req.Content) == 0 {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),\n\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})\n\t\treturn\n\t}\n\n\terr := uc.userService.UpdatePasswordWhenForgot(ctx, req)\n\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP())\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UserLogout user logout\n// @Summary user logout\n// @Description user logout\n// @Security ApiKeyAuth\n// @Tags User\n// @Accept json\n// @Produce json\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/user/logout [get]\nfunc (uc *UserController) UserLogout(ctx *gin.Context) {\n\taccessToken := middleware.ExtractToken(ctx)\n\tif len(accessToken) == 0 {\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\t_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)\n\t_ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken)\n\tvisitToken, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey)\n\t_ = uc.authService.RemoveUserVisitCacheInfo(ctx, visitToken)\n\thandler.HandleResponse(ctx, nil, nil)\n}\n\n// UserRegisterByEmail godoc\n// @Summary UserRegisterByEmail\n// @Description UserRegisterByEmail\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param data body schema.UserRegisterReq true \"UserRegisterReq\"\n// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}\n// @Router /answer/api/v1/user/register/email [post]\nfunc (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {\n\t// check whether site allow register or not\n\tsiteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)\n\t\treturn\n\t}\n\n\treq := &schema.UserRegisterReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tif !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)\n\t\treturn\n\t}\n\treq.IP = ctx.ClientIP()\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, req.IP, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)\n\tif len(errFields) > 0 {\n\t\tfor _, field := range errFields {\n\t\t\tfield.ErrorMsg = translator.\n\t\t\t\tTr(handler.GetLangByCtx(ctx), field.ErrorMsg)\n\t\t}\n\t\thandler.HandleResponse(ctx, err, errFields)\n\t} else {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t}\n}\n\n// UserVerifyEmail godoc\n// @Summary UserVerifyEmail\n// @Description UserVerifyEmail\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param code query string true \"code\" default()\n// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp}\n// @Router /answer/api/v1/user/email/verification [post]\nfunc (uc *UserController) UserVerifyEmail(ctx *gin.Context) {\n\treq := &schema.UserVerifyEmailReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)\n\tif len(req.Content) == 0 {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),\n\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.UserVerifyEmail(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP())\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserVerifyEmailSend godoc\n// @Summary UserVerifyEmailSend\n// @Description UserVerifyEmailSend\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param captcha_id query string false \"captcha_id\"  default()\n// @Param captcha_code query string false \"captcha_code\"  default()\n// @Success 200 {string} string \"\"\n// @Router /answer/api/v1/user/email/verification/send [post]\nfunc (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {\n\treq := &schema.UserVerifyEmailSendReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tuserInfo := middleware.GetUserInfoFromContext(ctx)\n\tif userInfo == nil {\n\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\treturn\n\t}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\terr := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UserModifyPassWord godoc\n// @Summary UserModifyPassWord\n// @Description UserModifyPassWord\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UserModifyPasswordReq  true \"UserModifyPasswordReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/user/password [put]\nfunc (uc *UserController) UserModifyPassWord(ctx *gin.Context) {\n\treq := &schema.UserModifyPasswordReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.AccessToken = middleware.ExtractToken(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID,\n\t\t\treq.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t\tuc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID)\n\t}\n\n\toldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !oldPassVerification {\n\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\tErrorField: \"old_pass\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),\n\t\t})\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)\n\t\treturn\n\t}\n\n\tif req.OldPass == req.Pass {\n\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\tErrorField: \"pass\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.NewPasswordSameAsPreviousSetting),\n\t\t})\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)\n\t\treturn\n\t}\n\terr = uc.userService.UserModifyPassword(ctx, req)\n\tif err == nil {\n\t\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, req.UserID)\n\t}\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UserUpdateInfo update user info\n// @Summary UserUpdateInfo update user info\n// @Description UserUpdateInfo update user info\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param Authorization header string true \"access-token\"\n// @Param data body schema.UpdateInfoRequest true \"UpdateInfoRequest\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/user/info [put]\nfunc (uc *UserController) UserUpdateInfo(ctx *gin.Context) {\n\treq := &schema.UpdateInfoRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\terrFields, err := uc.userService.UpdateInfo(ctx, req)\n\tfor _, field := range errFields {\n\t\tfield.ErrorMsg = translator.Tr(handler.GetLangByCtx(ctx), field.ErrorMsg)\n\t}\n\thandler.HandleResponse(ctx, err, errFields)\n}\n\n// UserUpdateInterface update user interface config\n// @Summary UserUpdateInterface update user interface config\n// @Description UserUpdateInterface update user interface config\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param Authorization header string true \"access-token\"\n// @Param data body schema.UpdateUserInterfaceRequest true \"UpdateInfoRequest\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/user/interface [put]\nfunc (uc *UserController) UserUpdateInterface(ctx *gin.Context) {\n\treq := &schema.UpdateUserInterfaceRequest{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserId = middleware.GetLoginUserIDFromContext(ctx)\n\terr := uc.userService.UserUpdateInterface(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// ActionRecord godoc\n// @Summary ActionRecord\n// @Description ActionRecord\n// @Tags User\n// @Param action query string true \"action\" Enums(login, e_mail, find_pass)\n// @Security ApiKeyAuth\n// @Success 200 {object} handler.RespBody{data=schema.ActionRecordResp}\n// @Router /answer/api/v1/user/action/record [get]\nfunc (uc *UserController) ActionRecord(ctx *gin.Context) {\n\treq := &schema.ActionRecordReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tuserinfo := middleware.GetUserInfoFromContext(ctx)\n\tif userinfo != nil {\n\t\treq.UserID = userinfo.UserID\n\t}\n\treq.IP = ctx.ClientIP()\n\tresp := &schema.ActionRecordResp{}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif isAdmin {\n\t\tresp.Verify = false\n\t\thandler.HandleResponse(ctx, nil, resp)\n\t} else {\n\t\tresp, err := uc.actionService.ActionRecord(ctx, req)\n\t\thandler.HandleResponse(ctx, err, resp)\n\t}\n}\n\n// GetUserNotificationConfig get user's notification config\n// @Summary get user's notification config\n// @Description get user's notification config\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Success 200 {object} handler.RespBody{data=schema.GetUserNotificationConfigResp}\n// @Router /answer/api/v1/user/notification/config [post]\nfunc (uc *UserController) GetUserNotificationConfig(ctx *gin.Context) {\n\tuserID := middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := uc.userNotificationConfigService.GetUserNotificationConfig(ctx, userID)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateUserNotificationConfig update user's notification config\n// @Summary update user's notification config\n// @Description update user's notification config\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateUserNotificationConfigReq true \"UpdateUserNotificationConfigReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/user/notification/config [put]\nfunc (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) {\n\treq := &schema.UpdateUserNotificationConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\terr := uc.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UserChangeEmailSendCode send email to the user email then change their email\n// @Summary send email to the user email then change their email\n// @Description send email to the user email then change their email\n// @Security ApiKeyAuth\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param data body schema.UserChangeEmailSendCodeReq true \"UserChangeEmailSendCodeReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/user/email/change/code [post]\nfunc (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {\n\treq := &schema.UserChangeEmailSendCodeReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\t// If the user is not logged in, the api cannot be used.\n\t// If the user email is not verified, that also can use this api to modify the email.\n\tif len(req.UserID) == 0 {\n\t\thandler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)\n\t\treturn\n\t}\n\t// check whether email allow register or not\n\tsiteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)\n\t\treturn\n\t}\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\n\tif !isAdmin {\n\t\tcaptchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tuc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresp, err := uc.userService.UserChangeEmailSendCode(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t\treturn\n\t}\n\tif !isAdmin {\n\t\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, ctx.ClientIP())\n\t}\n\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UserChangeEmailVerify user change email verification\n// @Summary user change email verification\n// @Description user change email verification\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UserChangeEmailVerifyReq true \"UserChangeEmailVerifyReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/user/email [put]\nfunc (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {\n\treq := &schema.UserChangeEmailVerifyReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)\n\tif len(req.Content) == 0 {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),\n\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content)\n\tuc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP())\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserRanking get user ranking\n// @Summary get user ranking\n// @Description get user ranking\n// @Tags User\n// @Accept json\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.UserRankingResp}\n// @Router /answer/api/v1/user/ranking [get]\nfunc (uc *UserController) UserRanking(ctx *gin.Context) {\n\tresp, err := uc.userService.UserRanking(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserStaff get user staff\n// @Summary get user staff\n// @Description get user staff\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param username query string true \"username\"\n// @Param page_size query string true \"page_size\"\n// @Success 200 {object} handler.RespBody{data=schema.GetUserStaffResp}\n// @Router /answer/api/v1/user/staff [get]\nfunc (uc *UserController) UserStaff(ctx *gin.Context) {\n\treq := &schema.GetUserStaffReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.GetUserStaff(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UserUnsubscribeNotification unsubscribe notification\n// @Summary unsubscribe notification\n// @Description unsubscribe notification\n// @Tags User\n// @Accept json\n// @Produce json\n// @Param data body schema.UserUnsubscribeNotificationReq true \"UserUnsubscribeNotificationReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/api/v1/user/notification/unsubscribe [put]\nfunc (uc *UserController) UserUnsubscribeNotification(ctx *gin.Context) {\n\treq := &schema.UserUnsubscribeNotificationReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)\n\tif len(req.Content) == 0 {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),\n\t\t\t&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})\n\t\treturn\n\t}\n\n\terr := uc.userService.UserUnsubscribeNotification(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// SearchUserListByName godoc\n// @Summary SearchUserListByName\n// @Description SearchUserListByName\n// @Tags User\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param username query string true \"username\"\n// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}\n// @Router /answer/api/v1/user/info/search [get]\nfunc (uc *UserController) SearchUserListByName(ctx *gin.Context) {\n\treq := &schema.GetOtherUserInfoByUsernameReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tresp, err := uc.userService.SearchUserListByName(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\nfunc (uc *UserController) setVisitCookies(ctx *gin.Context, visitToken string, force bool) {\n\tif !force {\n\t\tcookie, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey)\n\t\t// If the cookie is the same as the visitToken, no need to set it again\n\t\tif cookie == visitToken {\n\t\t\treturn\n\t\t}\n\t}\n\tgeneral, err := uc.siteInfoCommonService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site general error: %v\", err)\n\t\treturn\n\t}\n\tparsedURL, err := url.Parse(general.SiteUrl)\n\tif err != nil {\n\t\tlog.Errorf(\"parse url error: %v\", err)\n\t\treturn\n\t}\n\tctx.SetCookie(constant.UserVisitCookiesCacheKey,\n\t\tvisitToken, constant.UserVisitCacheTime, \"/\", parsedURL.Hostname(), true, true)\n}\n"
  },
  {
    "path": "internal/controller/user_plugin_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/segmentfault/pacman/errors\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// UserPluginController role controller\ntype UserPluginController struct {\n\tpluginCommonService *plugin_common.PluginCommonService\n}\n\n// NewUserPluginController new controller\nfunc NewUserPluginController(pluginCommonService *plugin_common.PluginCommonService) *UserPluginController {\n\treturn &UserPluginController{pluginCommonService: pluginCommonService}\n}\n\n// GetUserPluginList get plugin list that used for user.\n// @Summary get plugin list that used for user.\n// @Description get plugin list that used for user.\n// @Tags UserPlugin\n// @Security ApiKeyAuth\n// @Accept  json\n// @Produce  json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetUserPluginListResp}\n// @Router /answer/api/v1/user/plugin/configs [get]\nfunc (pc *UserPluginController) GetUserPluginList(ctx *gin.Context) {\n\tresp := make([]*schema.GetUserPluginListResp, 0)\n\t_ = plugin.CallUserConfig(func(base plugin.UserConfig) error {\n\t\tinfo := base.Info()\n\t\tif plugin.StatusManager.IsEnabled(info.SlugName) {\n\t\t\tresp = append(resp, &schema.GetUserPluginListResp{\n\t\t\t\tName:     info.Name.Translate(ctx),\n\t\t\t\tSlugName: info.SlugName,\n\t\t\t})\n\t\t}\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetUserPluginConfig get user plugin config\n// @Summary get user plugin config\n// @Description get user plugin config\n// @Tags UserPlugin\n// @Security ApiKeyAuth\n// @Produce  json\n// @Param plugin_slug_name query string true \"plugin_slug_name\"\n// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}\n// @Router /answer/api/v1/user/plugin/config [get]\nfunc (pc *UserPluginController) GetUserPluginConfig(ctx *gin.Context) {\n\treq := &schema.GetUserPluginConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp := &schema.GetUserPluginConfigResp{}\n\t_ = plugin.CallUserConfig(func(fn plugin.UserConfig) error {\n\t\tif fn.Info().SlugName != req.PluginSlugName {\n\t\t\treturn nil\n\t\t}\n\t\tinfo := fn.Info()\n\t\tresp.Name = info.Name.Translate(ctx)\n\t\tresp.SlugName = info.SlugName\n\t\tresp.SetConfigFields(ctx, fn.UserConfigFields())\n\t\treturn nil\n\t})\n\n\tconfigValue, err := pc.pluginCommonService.GetUserPluginConfig(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif len(configValue) > 0 {\n\t\tconfigValueMapping := make(map[string]any)\n\t\t_ = json.Unmarshal([]byte(configValue), &configValueMapping)\n\t\tfor _, field := range resp.ConfigFields {\n\t\t\tif value, ok := configValueMapping[field.Name]; ok {\n\t\t\t\tfield.Value = value\n\t\t\t}\n\t\t}\n\t}\n\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdatePluginUserConfig update user plugin config\n// @Summary update user plugin config\n// @Description update user plugin config\n// @Tags UserPlugin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateUserPluginConfigReq true \"UpdatePluginConfigReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/api/v1/user/plugin/config [put]\nfunc (pc *UserPluginController) UpdatePluginUserConfig(ctx *gin.Context) {\n\treq := &schema.UpdateUserPluginConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tif !plugin.StatusManager.IsEnabled(req.PluginSlugName) {\n\t\thandler.HandleResponse(ctx, errors.New(http.StatusBadRequest, reason.RequestFormatError), nil)\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tconfigFields, _ := json.Marshal(req.ConfigFields)\n\terr := plugin.CallUserConfig(func(fn plugin.UserConfig) error {\n\t\tif fn.Info().SlugName == req.PluginSlugName {\n\t\t\treturn fn.UserConfigReceiver(req.UserID, configFields)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\terr = pc.pluginCommonService.UpdatePluginUserConfig(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller/vote_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// VoteController activity controller\ntype VoteController struct {\n\tVoteService   *content.VoteService\n\trankService   *rank.RankService\n\tactionService *action.CaptchaService\n}\n\n// NewVoteController new controller\nfunc NewVoteController(\n\tvoteService *content.VoteService,\n\trankService *rank.RankService,\n\tactionService *action.CaptchaService,\n) *VoteController {\n\treturn &VoteController{\n\t\tVoteService:   voteService,\n\t\trankService:   rankService,\n\t\tactionService: actionService,\n\t}\n}\n\n// VoteUp godoc\n// @Summary vote up\n// @Description add vote\n// @Tags Activity\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.VoteReq true \"vote\"\n// @Success 200 {object} handler.RespBody{data=schema.VoteResp}\n// @Router /answer/api/v1/vote/up [post]\nfunc (vc *VoteController) VoteUp(ctx *gin.Context) {\n\treq := &schema.VoteReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tcan, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\tlang := handler.GetLangByCtx(ctx)\n\t\tmsg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)\n\t\treturn\n\t}\n\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\tif !isAdmin {\n\t\tcaptchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif !isAdmin {\n\t\tvc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID)\n\t}\n\tresp, err := vc.VoteService.VoteUp(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, schema.ErrTypeToast)\n\t} else {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t}\n}\n\n// VoteDown godoc\n// @Summary vote down\n// @Description add vote\n// @Tags Activity\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.VoteReq true \"vote\"\n// @Success 200 {object} handler.RespBody{data=schema.VoteResp}\n// @Router /answer/api/v1/vote/down [post]\nfunc (vc *VoteController) VoteDown(ctx *gin.Context) {\n\treq := &schema.VoteReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\tisAdmin := middleware.GetUserIsAdminModerator(ctx)\n\n\tcan, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tif !can {\n\t\tlang := handler.GetLangByCtx(ctx)\n\t\tmsg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)\n\t\treturn\n\t}\n\n\tif !isAdmin {\n\t\tcaptchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode)\n\t\tif !captchaPass {\n\t\t\terrFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"captcha_code\",\n\t\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.CaptchaVerificationFailed),\n\t\t\t})\n\t\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)\n\t\t\treturn\n\t\t}\n\t}\n\tif !isAdmin {\n\t\tvc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID)\n\t}\n\tresp, err := vc.VoteService.VoteDown(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, schema.ErrTypeToast)\n\t} else {\n\t\thandler.HandleResponse(ctx, err, resp)\n\t}\n}\n\n// UserVotes user votes\n// @Summary get user personal votes\n// @Description get user personal votes\n// @Tags Activity\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetVoteWithPageResp}}\n// @Router /answer/api/v1/personal/vote/page [get]\nfunc (vc *VoteController) UserVotes(ctx *gin.Context) {\n\treq := schema.GetVoteWithPageReq{}\n\tif handler.BindAndCheck(ctx, &req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := vc.VoteService.ListUserVotes(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller_admin/ai_conversation_admin_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/ai_conversation\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// AIConversationAdminController ai conversation admin controller\ntype AIConversationAdminController struct {\n\taiConversationService ai_conversation.AIConversationService\n\tfeatureToggleSvc      *feature_toggle.FeatureToggleService\n}\n\n// NewAIConversationAdminController new AI conversation admin controller\nfunc NewAIConversationAdminController(\n\taiConversationService ai_conversation.AIConversationService,\n\tfeatureToggleSvc *feature_toggle.FeatureToggleService,\n) *AIConversationAdminController {\n\treturn &AIConversationAdminController{\n\t\taiConversationService: aiConversationService,\n\t\tfeatureToggleSvc:      featureToggleSvc,\n\t}\n}\n\nfunc (ctrl *AIConversationAdminController) ensureEnabled(ctx *gin.Context) bool {\n\tif ctrl.featureToggleSvc == nil {\n\t\treturn true\n\t}\n\tif err := ctrl.featureToggleSvc.EnsureEnabled(ctx, feature_toggle.FeatureAIChatbot); err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// GetConversationList gets conversation list\n// @Summary get conversation list for admin\n// @Description get conversation list for admin\n// @Tags ai-conversation-admin\n// @Accept json\n// @Produce json\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.AIConversationAdminListItem}}\n// @Router /answer/admin/api/ai/conversation/page [get]\nfunc (ctrl *AIConversationAdminController) GetConversationList(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationAdminListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := ctrl.aiConversationService.GetConversationListForAdmin(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetConversationDetail get conversation detail\n// @Summary get conversation detail for admin\n// @Description get conversation detail for admin\n// @Tags ai-conversation-admin\n// @Accept json\n// @Produce json\n// @Param conversation_id query string true \"conversation id\"\n// @Success 200 {object} handler.RespBody{data=schema.AIConversationAdminDetailResp}\n// @Router /answer/admin/api/ai/conversation [get]\nfunc (ctrl *AIConversationAdminController) GetConversationDetail(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationAdminDetailReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := ctrl.aiConversationService.GetConversationDetailForAdmin(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// DeleteConversation delete conversation\n// @Summary delete conversation for admin\n// @Description delete conversation and its related records for admin\n// @Tags ai-conversation-admin\n// @Accept json\n// @Produce json\n// @Param data body schema.AIConversationAdminDeleteReq true \"apikey\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/ai/conversation [delete]\nfunc (ctrl *AIConversationAdminController) DeleteConversation(ctx *gin.Context) {\n\tif !ctrl.ensureEnabled(ctx) {\n\t\treturn\n\t}\n\treq := &schema.AIConversationAdminDeleteReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := ctrl.aiConversationService.DeleteConversationForAdmin(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller_admin/badge_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype BadgeController struct {\n\tbadgeService *badge.BadgeService\n}\n\nfunc NewBadgeController(badgeService *badge.BadgeService) *BadgeController {\n\treturn &BadgeController{\n\t\tbadgeService: badgeService,\n\t}\n}\n\n// GetBadgeList list all badges by page\n// @Summary list all badges by page\n// @Description list all badges by page\n// @Tags AdminBadge\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param page query int false \"page\"\n// @Param page_size query int false \"page size\"\n// @Param status query string false \"badge status\" Enums(, active, inactive)\n// @Param q query string false \"search param\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListPagedResp}\n// @Router /answer/admin/api/badges [get]\nfunc (b *BadgeController) GetBadgeList(ctx *gin.Context) {\n\treq := &schema.GetBadgeListPagedReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, total, err := b.badgeService.ListPaged(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\thandler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp))\n}\n\n// UpdateBadgeStatus update badge status\n// @Summary update badge status\n// @Description update badge status\n// @Tags AdminBadge\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdateBadgeStatusReq true \"UpdateBadgeStatusReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/badge/status [put]\nfunc (b *BadgeController) UpdateBadgeStatus(ctx *gin.Context) {\n\treq := &schema.UpdateBadgeStatusReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := b.badgeService.UpdateStatus(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller_admin/controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport \"github.com/google/wire\"\n\n// ProviderSetController is controller providers.\nvar ProviderSetController = wire.NewSet(\n\tNewUserAdminController,\n\tNewThemeController,\n\tNewSiteInfoController,\n\tNewRoleController,\n\tNewPluginController,\n\tNewBadgeController,\n\tNewAdminAPIKeyController,\n\tNewAIConversationAdminController,\n)\n"
  },
  {
    "path": "internal/controller_admin/e_api_key_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/apikey\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// AdminAPIKeyController site info controller\ntype AdminAPIKeyController struct {\n\tapiKeyService *apikey.APIKeyService\n}\n\n// NewAdminAPIKeyController new site info controller\nfunc NewAdminAPIKeyController(apiKeyService *apikey.APIKeyService) *AdminAPIKeyController {\n\treturn &AdminAPIKeyController{\n\t\tapiKeyService: apiKeyService,\n\t}\n}\n\n// GetAllAPIKeys get all api keys\n// @Summary get all api keys\n// @Description get all api keys\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetAPIKeyResp}\n// @Router /answer/admin/api/api-key/all [get]\nfunc (sc *AdminAPIKeyController) GetAllAPIKeys(ctx *gin.Context) {\n\tresp, err := sc.apiKeyService.GetAPIKeyList(ctx, &schema.GetAPIKeyReq{})\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// AddAPIKey add apikey\n// @Summary add apikey\n// @Description add apikey\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.AddAPIKeyReq true \"apikey\"\n// @Success 200 {object} handler.RespBody{data=schema.AddAPIKeyResp}\n// @Router /answer/admin/api/api-key [post]\nfunc (sc *AdminAPIKeyController) AddAPIKey(ctx *gin.Context) {\n\treq := &schema.AddAPIKeyReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := sc.apiKeyService.AddAPIKey(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateAPIKey update apikey\n// @Summary update apikey\n// @Description update apikey\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.UpdateAPIKeyReq true \"apikey\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/api-key [put]\nfunc (sc *AdminAPIKeyController) UpdateAPIKey(ctx *gin.Context) {\n\treq := &schema.UpdateAPIKeyReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := sc.apiKeyService.UpdateAPIKey(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// DeleteAPIKey delete apikey\n// @Summary delete apikey\n// @Description delete apikey\n// @Security ApiKeyAuth\n// @Tags admin\n// @Param data body schema.DeleteAPIKeyReq true \"apikey\"\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/api-key [delete]\nfunc (sc *AdminAPIKeyController) DeleteAPIKey(ctx *gin.Context) {\n\treq := &schema.DeleteAPIKeyReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := sc.apiKeyService.DeleteAPIKey(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller_admin/plugin_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// PluginController role controller\ntype PluginController struct {\n\tpluginCommonService *plugin_common.PluginCommonService\n}\n\n// NewPluginController new controller\nfunc NewPluginController(pluginCommonService *plugin_common.PluginCommonService) *PluginController {\n\treturn &PluginController{pluginCommonService: pluginCommonService}\n}\n\n// GetAllPluginStatus get all plugins status\n// @Summary get all plugins status\n// @Description get all plugins status\n// @Tags Plugin\n// @Accept  json\n// @Produce  json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp}\n// @Router /answer/api/v1/plugin/status [get]\nfunc (pc *PluginController) GetAllPluginStatus(ctx *gin.Context) {\n\tresp := make([]*schema.GetAllPluginStatusResp, 0)\n\t_ = plugin.CallBase(func(base plugin.Base) error {\n\t\tinfo := base.Info()\n\t\tresp = append(resp, &schema.GetAllPluginStatusResp{\n\t\t\tSlugName: info.SlugName,\n\t\t\tEnabled:  plugin.StatusManager.IsEnabled(info.SlugName),\n\t\t})\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetPluginList get plugin list\n// @Summary get plugin list\n// @Description get plugin list\n// @Tags AdminPlugin\n// @Security ApiKeyAuth\n// @Accept  json\n// @Produce  json\n// @Param status query string false \"status: active/inactive\"\n// @Param have_config query boolean false \"have config\"\n// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp}\n// @Router /answer/admin/api/plugins [get]\nfunc (pc *PluginController) GetPluginList(ctx *gin.Context) {\n\treq := &schema.GetPluginListReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tpluginConfigMapping := make(map[string]bool)\n\t_ = plugin.CallConfig(func(fn plugin.Config) error {\n\t\tif len(fn.ConfigFields()) > 0 {\n\t\t\tpluginConfigMapping[fn.Info().SlugName] = true\n\t\t}\n\t\treturn nil\n\t})\n\n\tresp := make([]*schema.GetPluginListResp, 0)\n\t_ = plugin.CallBase(func(base plugin.Base) error {\n\t\tinfo := base.Info()\n\t\tresp = append(resp, &schema.GetPluginListResp{\n\t\t\tName:        info.Name.Translate(ctx),\n\t\t\tSlugName:    info.SlugName,\n\t\t\tDescription: info.Description.Translate(ctx),\n\t\t\tVersion:     info.Version,\n\t\t\tEnabled:     plugin.StatusManager.IsEnabled(info.SlugName),\n\t\t\tHaveConfig:  pluginConfigMapping[info.SlugName],\n\t\t\tLink:        info.Link,\n\t\t})\n\t\treturn nil\n\t})\n\n\tif len(req.Status) > 0 {\n\t\tresp = pc.filterPluginByStatus(resp, req.Status)\n\t}\n\tif req.HaveConfig {\n\t\tresp = pc.filterNoConfigPlugin(resp)\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\nfunc (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp {\n\tresp := make([]*schema.GetPluginListResp, 0)\n\tfor _, t := range list {\n\t\tif t.HaveConfig {\n\t\t\tresp = append(resp, t)\n\t\t}\n\t}\n\treturn resp\n}\n\nfunc (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus,\n) []*schema.GetPluginListResp {\n\tresp := make([]*schema.GetPluginListResp, 0)\n\tfor _, t := range list {\n\t\tif status == schema.PluginStatusActive && t.Enabled {\n\t\t\tresp = append(resp, t)\n\t\t} else if status == schema.PluginStatusInactive && !t.Enabled {\n\t\t\tresp = append(resp, t)\n\t\t}\n\t}\n\treturn resp\n}\n\n// UpdatePluginStatus update plugin status\n// @Summary update plugin status\n// @Description update plugin status\n// @Tags AdminPlugin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdatePluginStatusReq true \"UpdatePluginStatusReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/plugin/status [put]\nfunc (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) {\n\treq := &schema.UpdatePluginStatusReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tplugin.StatusManager.Enable(req.PluginSlugName, req.Enabled)\n\terr := pc.pluginCommonService.UpdatePluginStatus(ctx)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetPluginConfig get plugin config\n// @Summary get plugin config\n// @Description get plugin config\n// @Tags AdminPlugin\n// @Security ApiKeyAuth\n// @Produce  json\n// @Param plugin_slug_name query string true \"plugin_slug_name\"\n// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}\n// @Router /answer/admin/api/plugin/config [get]\nfunc (pc *PluginController) GetPluginConfig(ctx *gin.Context) {\n\treq := &schema.GetPluginConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp := &schema.GetPluginConfigResp{}\n\t_ = plugin.CallBase(func(base plugin.Base) error {\n\t\tif base.Info().SlugName != req.PluginSlugName {\n\t\t\treturn nil\n\t\t}\n\t\tinfo := base.Info()\n\t\tresp.Name = info.Name.Translate(ctx)\n\t\tresp.SlugName = info.SlugName\n\t\tresp.Description = info.Description.Translate(ctx)\n\t\tresp.Version = info.Version\n\t\treturn nil\n\t})\n\n\t_ = plugin.CallConfig(func(fn plugin.Config) error {\n\t\tif fn.Info().SlugName != req.PluginSlugName {\n\t\t\treturn nil\n\t\t}\n\t\tresp.SetConfigFields(ctx, fn.ConfigFields())\n\t\treturn nil\n\t})\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// UpdatePluginConfig update plugin config\n// @Summary update plugin config\n// @Description update plugin config\n// @Tags AdminPlugin\n// @Accept json\n// @Produce json\n// @Security ApiKeyAuth\n// @Param data body schema.UpdatePluginConfigReq true \"UpdatePluginConfigReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/plugin/config [put]\nfunc (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) {\n\treq := &schema.UpdatePluginConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tconfigFields, _ := json.Marshal(req.ConfigFields)\n\terr := plugin.CallConfig(func(fn plugin.Config) error {\n\t\tif fn.Info().SlugName == req.PluginSlugName {\n\t\t\treturn fn.ConfigReceiver(configFields)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\n\terr = pc.pluginCommonService.UpdatePluginConfig(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller_admin/role_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\tservice \"github.com/apache/answer/internal/service/role\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// RoleController role controller\ntype RoleController struct {\n\troleService *service.RoleService\n}\n\n// NewRoleController new controller\nfunc NewRoleController(roleService *service.RoleService) *RoleController {\n\treturn &RoleController{roleService: roleService}\n}\n\n// GetRoleList get role list\n// @Summary get role list\n// @Description get role list\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetRoleResp}\n// @Router /answer/admin/api/roles [get]\nfunc (rc *RoleController) GetRoleList(ctx *gin.Context) {\n\treq := &schema.GetRoleResp{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tresp, err := rc.roleService.GetRoleList(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n"
  },
  {
    "path": "internal/controller_admin/siteinfo_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"html\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/siteinfo\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// SiteInfoController site info controller\ntype SiteInfoController struct {\n\tsiteInfoService *siteinfo.SiteInfoService\n}\n\n// NewSiteInfoController new site info controller\nfunc NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {\n\treturn &SiteInfoController{\n\t\tsiteInfoService: siteInfoService,\n\t}\n}\n\n// GetGeneral get site general information\n// @Summary get site general information\n// @Description get site general information\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}\n// @Router /answer/admin/api/siteinfo/general [get]\nfunc (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteGeneral(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetInterface get site interface\n// @Summary get site interface\n// @Description get site interface\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceSettingsResp}\n// @Router /answer/admin/api/siteinfo/interface [get]\nfunc (sc *SiteInfoController) GetInterface(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteInterface(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetUsersSettings get site interface\n// @Summary get site interface\n// @Description get site interface\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteUsersSettingsResp}\n// @Router /answer/admin/api/siteinfo/users-settings [get]\nfunc (sc *SiteInfoController) GetUsersSettings(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteUsersSettings(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteBranding get site interface\n// @Summary get site interface\n// @Description get site interface\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp}\n// @Router /answer/admin/api/siteinfo/branding [get]\nfunc (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteBranding(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteTag get site tags setting\n// @Summary get site tags setting\n// @Description get site tags setting\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteTagsResp}\n// @Router /answer/admin/api/siteinfo/tag [get]\nfunc (sc *SiteInfoController) GetSiteTag(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteTag(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteQuestion get site questions setting\n// @Summary get site questions setting\n// @Description get site questions setting\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteQuestionsResp}\n// @Router /answer/admin/api/siteinfo/question [get]\nfunc (sc *SiteInfoController) GetSiteQuestion(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteQuestion(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteAdvanced get site advanced setting\n// @Summary get site advanced setting\n// @Description get site advanced setting\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteAdvancedResp}\n// @Router /answer/admin/api/siteinfo/advanced [get]\nfunc (sc *SiteInfoController) GetSiteAdvanced(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteAdvanced(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSitePolicies Get the policies information for the site\n// @Summary Get the policies information for the site\n// @Description Get the policies information for the site\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SitePoliciesResp}\n// @Router /answer/admin/api/siteinfo/polices [get]\nfunc (sc *SiteInfoController) GetSitePolicies(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSitePolicies(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteSecurity Get the security information for the site\n// @Summary Get the security information for the site\n// @Description Get the security information for the site\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteSecurityResp}\n// @Router /answer/admin/api/siteinfo/security [get]\nfunc (sc *SiteInfoController) GetSiteSecurity(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteSecurity(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSeo get site seo information\n// @Summary get site seo information\n// @Description get site seo information\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteSeoResp}\n// @Router /answer/admin/api/siteinfo/seo [get]\nfunc (sc *SiteInfoController) GetSeo(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSeo(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteLogin get site info login config\n// @Summary get site info login config\n// @Description get site info login config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteLoginResp}\n// @Router /answer/admin/api/siteinfo/login [get]\nfunc (sc *SiteInfoController) GetSiteLogin(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteLogin(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteCustomCssHTML get site info custom html css config\n// @Summary get site info custom html css config\n// @Description get site info custom html css config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteCustomCssHTMLResp}\n// @Router /answer/admin/api/siteinfo/custom-css-html [get]\nfunc (sc *SiteInfoController) GetSiteCustomCssHTML(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteTheme get site info theme config\n// @Summary get site info theme config\n// @Description get site info theme config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteThemeResp}\n// @Router /answer/admin/api/siteinfo/theme [get]\nfunc (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteTheme(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetSiteUsers get site user config\n// @Summary get site user config\n// @Description get site user config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp}\n// @Router /answer/admin/api/siteinfo/users [get]\nfunc (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteUsers(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetRobots get site robots information\n// @Summary get site robots information\n// @Description get site robots information\n// @Tags site\n// @Produce json\n// @Success 200 {string} txt \"\"\n// @Router /robots.txt [get]\nfunc (sc *SiteInfoController) GetRobots(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSeo(ctx)\n\tif err != nil {\n\t\tctx.String(http.StatusOK, \"\")\n\t\treturn\n\t}\n\tctx.String(http.StatusOK, resp.Robots)\n}\n\n// GetCss get site custom CSS\n// @Summary get site custom CSS\n// @Description get site custom CSS\n// @Tags site\n// @Produce text/css\n// @Success 200 {string} css \"\"\n// @Router /custom.css [get]\nfunc (sc *SiteInfoController) GetCss(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)\n\tif err != nil {\n\t\tctx.String(http.StatusOK, \"\")\n\t\treturn\n\t}\n\tctx.Header(\"content-type\", \"text/css;charset=utf-8\")\n\tctx.String(http.StatusOK, resp.CustomCss)\n}\n\n// UpdateSeo update site seo information\n// @Summary update site seo information\n// @Description update site seo information\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteSeoReq true \"seo\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/seo [put]\nfunc (sc *SiteInfoController) UpdateSeo(ctx *gin.Context) {\n\treq := schema.SiteSeoReq{}\n\tif handler.BindAndCheck(ctx, &req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSeo(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateGeneral update site general information\n// @Summary update site general information\n// @Description update site general information\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteGeneralReq true \"general\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/general [put]\nfunc (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {\n\treq := schema.SiteGeneralReq{}\n\tif handler.BindAndCheck(ctx, &req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteGeneral(ctx, req)\n\treq.Name = html.UnescapeString(req.Name)\n\thandler.HandleResponse(ctx, err, req)\n}\n\n// UpdateInterface update site interface\n// @Summary update site info interface\n// @Description update site info interface\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteInterfaceReq true \"general\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/interface [put]\nfunc (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {\n\treq := schema.SiteInterfaceReq{}\n\tif handler.BindAndCheck(ctx, &req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteInterface(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateUsersSettings update users settings\n// @Summary update site info users settings\n// @Description update site info users settings\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteUsersSettingsReq true \"general\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/users-settings [put]\nfunc (sc *SiteInfoController) UpdateUsersSettings(ctx *gin.Context) {\n\treq := schema.SiteUsersSettingsReq{}\n\tif handler.BindAndCheck(ctx, &req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteUsersSettings(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateBranding update site branding\n// @Summary update site info branding\n// @Description update site info branding\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteBrandingReq true \"branding info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/branding [put]\nfunc (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) {\n\treq := &schema.SiteBrandingReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tcurrentBranding, getBrandingErr := sc.siteInfoService.GetSiteBranding(ctx)\n\tif getBrandingErr == nil {\n\t\tcleanUpErr := sc.siteInfoService.CleanUpRemovedBrandingFiles(ctx, req, currentBranding)\n\t\tif cleanUpErr != nil {\n\t\t\tlog.Errorf(\"failed to clean up removed branding file(s): %v\", cleanUpErr)\n\t\t}\n\t} else {\n\t\tlog.Errorf(\"failed to get current site branding: %v\", getBrandingErr)\n\t}\n\tsaveErr := sc.siteInfoService.SaveSiteBranding(ctx, req)\n\thandler.HandleResponse(ctx, saveErr, nil)\n}\n\n// UpdateSiteQuestion update site question settings\n// @Summary update site question settings\n// @Description update site question settings\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteQuestionsReq true \"questions settings\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/question [put]\nfunc (sc *SiteInfoController) UpdateSiteQuestion(ctx *gin.Context) {\n\treq := &schema.SiteQuestionsReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := sc.siteInfoService.SaveSiteQuestions(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateSiteTag update site tag settings\n// @Summary update site tag settings\n// @Description update site tag settings\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteTagsReq true \"tags settings\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/tag [put]\nfunc (sc *SiteInfoController) UpdateSiteTag(ctx *gin.Context) {\n\treq := &schema.SiteTagsReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.UserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\tresp, err := sc.siteInfoService.SaveSiteTags(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateSiteAdvanced update site advanced info\n// @Summary update site advanced info\n// @Description update site advanced info\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteAdvancedReq true \"advanced settings\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/advanced [put]\nfunc (sc *SiteInfoController) UpdateSiteAdvanced(ctx *gin.Context) {\n\treq := &schema.SiteAdvancedReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := sc.siteInfoService.SaveSiteAdvanced(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateSitePolices update site policies configuration\n// @Summary update site policies configuration\n// @Description update site policies configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SitePoliciesReq true \"write info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/polices [put]\nfunc (sc *SiteInfoController) UpdateSitePolices(ctx *gin.Context) {\n\treq := &schema.SitePoliciesReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSitePolicies(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateSiteSecurity update site security configuration\n// @Summary update site security configuration\n// @Description update site security configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteSecurityReq true \"write info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/security [put]\nfunc (sc *SiteInfoController) UpdateSiteSecurity(ctx *gin.Context) {\n\treq := &schema.SiteSecurityReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteSecurity(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateSiteLogin update site login\n// @Summary update site login\n// @Description update site login\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteLoginReq true \"login info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/login [put]\nfunc (sc *SiteInfoController) UpdateSiteLogin(ctx *gin.Context) {\n\treq := &schema.SiteLoginReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteLogin(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateSiteCustomCssHTML update site custom css html config\n// @Summary update site custom css html config\n// @Description update site custom css html config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteCustomCssHTMLReq true \"login info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/custom-css-html [put]\nfunc (sc *SiteInfoController) UpdateSiteCustomCssHTML(ctx *gin.Context) {\n\treq := &schema.SiteCustomCssHTMLReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteCustomCssHTML(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// SaveSiteTheme update site custom css html config\n// @Summary update site custom css html config\n// @Description update site custom css html config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteThemeReq true \"login info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/theme [put]\nfunc (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {\n\treq := &schema.SiteThemeReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteTheme(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateSiteUsers update site config about users\n// @Summary update site info config about users\n// @Description update site info config about users\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SiteUsersReq true \"users info\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/siteinfo/users [put]\nfunc (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) {\n\treq := &schema.SiteUsersReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.SaveSiteUsers(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetSMTPConfig get smtp config\n// @Summary GetSMTPConfig get smtp config\n// @Description GetSMTPConfig get smtp config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.GetSMTPConfigResp}\n// @Router /answer/admin/api/setting/smtp [get]\nfunc (sc *SiteInfoController) GetSMTPConfig(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSMTPConfig(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateSMTPConfig update smtp config\n// @Summary update smtp config\n// @Description update smtp config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.UpdateSMTPConfigReq true \"smtp config\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/setting/smtp [put]\nfunc (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) {\n\treq := &schema.UpdateSMTPConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.UpdateSMTPConfig(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetPrivilegesConfig get privileges config\n// @Summary GetPrivilegesConfig get privileges config\n// @Description GetPrivilegesConfig get privileges config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp}\n// @Router /answer/admin/api/setting/privileges [get]\nfunc (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetPrivilegesConfig(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdatePrivilegesConfig update privileges config\n// @Summary update privileges config\n// @Description update privileges config\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.UpdatePrivilegesConfigReq true \"config\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/setting/privileges [put]\nfunc (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) {\n\treq := &schema.UpdatePrivilegesConfigReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\terr := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetAIConfig get AI configuration\n// @Summary get AI configuration\n// @Description get AI configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteAIResp}\n// @Router /answer/admin/api/ai-config [get]\nfunc (sc *SiteInfoController) GetAIConfig(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteAI(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateAIConfig update AI configuration\n// @Summary update AI configuration\n// @Description update AI configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Param data body schema.SiteAIReq true \"AI config\"\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/ai-config [put]\nfunc (sc *SiteInfoController) UpdateAIConfig(ctx *gin.Context) {\n\treq := &schema.SiteAIReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := sc.siteInfoService.SaveSiteAI(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// GetAIProvider get AI provider configuration\n// @Summary get AI provider configuration\n// @Description get AI provider configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetAIProviderResp}\n// @Router /answer/admin/api/ai-provider [get]\nfunc (sc *SiteInfoController) GetAIProvider(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetAIProvider(ctx)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// RequestAIModels get AI models\n// @Summary get AI models\n// @Description get AI models\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]schema.GetAIModelResp}\n// @Router /answer/admin/api/ai-models [post]\nfunc (sc *SiteInfoController) RequestAIModels(ctx *gin.Context) {\n\treq := &schema.GetAIModelsReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\tresp, err := sc.siteInfoService.GetAIModels(ctx, req)\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// GetMCPConfig get MCP configuration\n// @Summary get MCP configuration\n// @Description get MCP configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=schema.SiteMCPResp}\n// @Router /answer/admin/api/mcp-config [get]\nfunc (sc *SiteInfoController) GetMCPConfig(ctx *gin.Context) {\n\tresp, err := sc.siteInfoService.GetSiteMCP(ctx)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateMCPConfig update MCP configuration\n// @Summary update MCP configuration\n// @Description update MCP configuration\n// @Security ApiKeyAuth\n// @Tags admin\n// @Param data body schema.SiteMCPReq true \"MCP config\"\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/mcp-config [put]\nfunc (sc *SiteInfoController) UpdateMCPConfig(ctx *gin.Context) {\n\treq := &schema.SiteMCPReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := sc.siteInfoService.SaveSiteMCP(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/controller_admin/theme_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype ThemeController struct{}\n\n// NewThemeController new theme controller.\nfunc NewThemeController() *ThemeController {\n\treturn &ThemeController{}\n}\n\n// GetThemeOptions godoc\n// @Summary Get theme options\n// @Description Get theme options\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /answer/admin/api/theme/options [get]\nfunc (t *ThemeController) GetThemeOptions(ctx *gin.Context) {\n\thandler.HandleResponse(ctx, nil, schema.GetThemeOptions)\n}\n"
  },
  {
    "path": "internal/controller_admin/user_backyard_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage controller_admin\n\nimport (\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/user_admin\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// UserAdminController user controller\ntype UserAdminController struct {\n\tuserService *user_admin.UserAdminService\n}\n\n// NewUserAdminController new controller\nfunc NewUserAdminController(userService *user_admin.UserAdminService) *UserAdminController {\n\treturn &UserAdminController{userService: userService}\n}\n\n// UpdateUserStatus update user\n// @Summary update user\n// @Description update user\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.UpdateUserStatusReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/user/status [put]\nfunc (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {\n\tif u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\treturn\n\t}\n\treq := &schema.UpdateUserStatusReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := uc.userService.UpdateUserStatus(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// UpdateUserRole update user role\n// @Summary update user role\n// @Description update user role\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.UpdateUserRoleReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/user/role [put]\nfunc (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) {\n\treq := &schema.UpdateUserRoleReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := uc.userService.UpdateUserRole(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// AddUser add user\n// @Summary add user\n// @Description add user\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.AddUserReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/user [post]\nfunc (uc *UserAdminController) AddUser(ctx *gin.Context) {\n\treq := &schema.AddUserReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := uc.userService.AddUser(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// AddUsers add users\n// @Summary add users\n// @Description add users\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.AddUsersReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/users [post]\nfunc (uc *UserAdminController) AddUsers(ctx *gin.Context) {\n\treq := &schema.AddUsersReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.AddUsers(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// UpdateUserPassword update user password\n// @Summary update user password\n// @Description update user password\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.UpdateUserPasswordReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/user/password [put]\nfunc (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) {\n\treq := &schema.UpdateUserPasswordReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)\n\n\terr := uc.userService.UpdateUserPassword(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// EditUserProfile edit user profile\n// @Summary edit user profile\n// @Description edit user profile\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.EditUserProfileReq true \"user\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/user/profile [put]\nfunc (uc *UserAdminController) EditUserProfile(ctx *gin.Context) {\n\treq := &schema.EditUserProfileReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\treq.IsAdmin = middleware.GetUserIsAdminModerator(ctx)\n\tif !req.IsAdmin {\n\t\thandler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)\n\t\treturn\n\t}\n\n\terrFields, err := uc.userService.EditUserProfile(ctx, req)\n\tfor _, field := range errFields {\n\t\tfield.ErrorMsg = translator.Tr(handler.GetLangByCtx(ctx), field.ErrorMsg)\n\t}\n\thandler.HandleResponse(ctx, err, errFields)\n}\n\n// GetUserPage get user page\n// @Summary get user page\n// @Description get user page\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param page query int false \"page size\"\n// @Param page_size query int false \"page size\"\n// @Param query query string false \"search query: email, username or id:[id]\"\n// @Param staff query bool false \"staff user\"\n// @Param status query string false \"user status\" Enums(suspended, deleted, inactive)\n// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}}\n// @Router /answer/admin/api/users/page [get]\nfunc (uc *UserAdminController) GetUserPage(ctx *gin.Context) {\n\treq := &schema.GetUserPageReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.GetUserPage(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// GetUserActivation get user activation\n// @Summary get user activation\n// @Description get user activation\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param user_id query string true \"user id\"\n// @Success 200 {object} handler.RespBody{data=schema.GetUserActivationResp}\n// @Router /answer/admin/api/user/activation [get]\nfunc (uc *UserAdminController) GetUserActivation(ctx *gin.Context) {\n\treq := &schema.GetUserActivationReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp, err := uc.userService.GetUserActivation(ctx, req)\n\thandler.HandleResponse(ctx, err, resp)\n}\n\n// SendUserActivation send user activation\n// @Summary send user activation\n// @Description send user activation\n// @Security ApiKeyAuth\n// @Tags admin\n// @Produce json\n// @Param data body schema.SendUserActivationReq true \"SendUserActivationReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/users/activation [post]\nfunc (uc *UserAdminController) SendUserActivation(ctx *gin.Context) {\n\treq := &schema.SendUserActivationReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := uc.userService.SendUserActivation(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n\n// DeletePermanently delete permanently\n// @Summary delete permanently\n// @Description delete permanently\n// @Security ApiKeyAuth\n// @Tags admin\n// @Accept json\n// @Produce json\n// @Param data body schema.DeletePermanentlyReq true \"DeletePermanentlyReq\"\n// @Success 200 {object} handler.RespBody\n// @Router /answer/admin/api/delete/permanently [delete]\nfunc (uc *UserAdminController) DeletePermanently(ctx *gin.Context) {\n\treq := &schema.DeletePermanentlyReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\terr := uc.userService.DeletePermanently(ctx, req)\n\thandler.HandleResponse(ctx, err, nil)\n}\n"
  },
  {
    "path": "internal/entity/activity_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tActivityAvailable = 0\n\tActivityCancelled = 1\n)\n\n// Activity activity\ntype Activity struct {\n\tID               string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt        time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt        time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tCancelledAt      time.Time `xorm:\"TIMESTAMP cancelled_at\"`\n\tUserID           string    `xorm:\"not null index BIGINT(20) user_id\"`\n\tTriggerUserID    int64     `xorm:\"not null default 0 index BIGINT(20) trigger_user_id\"`\n\tObjectID         string    `xorm:\"not null default 0 index BIGINT(20) object_id\"`\n\tOriginalObjectID string    `xorm:\"not null default 0 BIGINT(20) original_object_id\"`\n\tActivityType     int       `xorm:\"not null INT(11) activity_type\"`\n\tCancelled        int       `xorm:\"not null default 0 TINYINT(4) cancelled\"`\n\tRank             int       `xorm:\"not null default 0 INT(11) rank\"`\n\tHasRank          int       `xorm:\"not null default 0 TINYINT(4) has_rank\"`\n\tRevisionID       int64     `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n}\n\ntype ActivityRankSum struct {\n\tRank int `xorm:\"not null default 0 INT(11) rank\"`\n}\n\ntype ActivityUserRankStat struct {\n\tUserID string `xorm:\"user_id\"`\n\tRank   int    `xorm:\"rank_amount\"`\n}\n\ntype ActivityUserVoteStat struct {\n\tUserID    string `xorm:\"user_id\"`\n\tVoteCount int    `xorm:\"vote_count\"`\n}\n\n// TableName activity table name\nfunc (Activity) TableName() string {\n\treturn \"activity\"\n}\n"
  },
  {
    "path": "internal/entity/ai_conversation.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// AIConversation AI\ntype AIConversation struct {\n\tID             int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt      time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tConversationID string    `xorm:\"not null unique VARCHAR(255) conversation_id\"`\n\tTopic          string    `xorm:\"not null MEDIUMTEXT topic\"`\n\tUserID         string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n}\n\n// TableName returns the table name\nfunc (AIConversation) TableName() string {\n\treturn \"ai_conversation\"\n}\n"
  },
  {
    "path": "internal/entity/ai_conversation_record.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// AIConversationRecord AI Conversation Record\ntype AIConversationRecord struct {\n\tID               int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt        time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt        time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tConversationID   string    `xorm:\"not null VARCHAR(255) conversation_id\"`\n\tChatCompletionID string    `xorm:\"not null VARCHAR(255) chat_completion_id\"`\n\tRole             string    `xorm:\"not null default '' VARCHAR(128) role\"`\n\tContent          string    `xorm:\"not null MEDIUMTEXT content\"`\n\tHelpful          int       `xorm:\"not null default 0 INT(11) helpful\"`\n\tUnhelpful        int       `xorm:\"not null default 0 INT(11) unhelpful\"`\n}\n\n// TableName returns the table name\nfunc (AIConversationRecord) TableName() string {\n\treturn \"ai_conversation_record\"\n}\n"
  },
  {
    "path": "internal/entity/answer_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tAnswerSearchOrderByDefault = \"default\"\n\tAnswerSearchOrderByTime    = \"updated\"\n\tAnswerSearchOrderByVote    = \"vote\"\n\tAnswerSearchOrderByTimeAsc = \"created\"\n\n\tAnswerStatusAvailable = 1\n\tAnswerStatusDeleted   = 10\n\tAnswerStatusPending   = 11\n)\n\nvar AdminAnswerSearchStatus = map[string]int{\n\t\"available\": AnswerStatusAvailable,\n\t\"deleted\":   AnswerStatusDeleted,\n\t\"pending\":   AnswerStatusPending,\n}\n\n// Answer answer\ntype Answer struct {\n\tID             string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated_at TIMESTAMP\"`\n\tQuestionID     string    `xorm:\"not null default 0 BIGINT(20) question_id\"`\n\tUserID         string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tLastEditUserID string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\tOriginalText   string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText     string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\tStatus         int       `xorm:\"not null default 1 INT(11) status\"`\n\tAccepted       int       `xorm:\"not null default 1 INT(11) adopted\"`\n\tCommentCount   int       `xorm:\"not null default 0 INT(11) comment_count\"`\n\tVoteCount      int       `xorm:\"not null default 0 INT(11) vote_count\"`\n\tRevisionID     string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n}\n\ntype AnswerSearch struct {\n\tAnswer\n\tIncludeDeleted bool   `json:\"include_deleted\"`\n\tLoginUserID    string `json:\"login_user_id\"`\n\tOrder          string `json:\"order_by\"`                   // default or updated\n\tPage           int    `json:\"page\" form:\"page\"`           // Query number of pages\n\tPageSize       int    `json:\"page_size\" form:\"page_size\"` // Search page size\n}\n\ntype PersonalAnswerPageQueryCond struct {\n\tPage        int\n\tPageSize    int\n\tUserID      string\n\tOrder       string\n\tShowPending bool\n}\n\n// TableName answer table name\nfunc (Answer) TableName() string {\n\treturn \"answer\"\n}\n"
  },
  {
    "path": "internal/entity/api_key_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"time\"\n)\n\n// APIKey entity\ntype APIKey struct {\n\tID          int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt   time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt   time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tLastUsedAt  time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP last_used_at\"`\n\tDescription string    `xorm:\"not null MEDIUMTEXT description\"`\n\tAccessKey   string    `xorm:\"not null unique VARCHAR(255) access_key\"`\n\tScope       string    `xorm:\"not null VARCHAR(255) scope\"`\n\tUserID      string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tHidden      int       `xorm:\"not null default 0 INT(11) hidden\"`\n}\n\n// TableName category table name\nfunc (c *APIKey) TableName() string {\n\treturn \"api_key\"\n}\n"
  },
  {
    "path": "internal/entity/auth_user_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\n// UserCacheInfo User Cache Information\ntype UserCacheInfo struct {\n\tUserID      string `json:\"user_id\"`\n\tUserStatus  int    `json:\"user_status\"`\n\tEmailStatus int    `json:\"email_status\"`\n\tRoleID      int    `json:\"role_id\"`\n\tExternalID  string `json:\"external_id\"`\n\tVisitToken  string `json:\"visit_token\"`\n}\n"
  },
  {
    "path": "internal/entity/badge_award_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tIsBadgeNotDeleted = 0\n\tIsBadgeDeleted    = 1\n\n\tBadgeEmptyAwardKey = \"0\"\n)\n\n// BadgeAward badge_award\ntype BadgeAward struct {\n\tID             string    `xorm:\"not null pk BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tUserID         string    `xorm:\"not null index BIGINT(20) user_id\"`\n\tBadgeID        string    `xorm:\"not null index BIGINT(20) badge_id\"`\n\tAwardKey       string    `xorm:\"not null index VARCHAR(64) award_key\"`\n\tBadgeGroupID   int64     `xorm:\"not null index BIGINT(20) badge_group_id\"`\n\tIsBadgeDeleted int8      `xorm:\"not null TINYINT(1) is_badge_deleted\"`\n}\n\n// TableName badge_award table name\nfunc (BadgeAward) TableName() string {\n\treturn \"badge_award\"\n}\n\ntype BadgeEarnedCount struct {\n\tBadgeID     string `xorm:\"badge_id\"`\n\tEarnedCount int64  `xorm:\"earned_count\"`\n}\n\n// TableName badge_award table name\nfunc (BadgeEarnedCount) TableName() string {\n\treturn \"badge_award\"\n}\n\ntype BadgeAwardRecent struct {\n\tCreated        time.Time `xorm:\"created\"`\n\tBadgeID        string    `xorm:\"badge_id\"`\n\tAwardKey       string    `xorm:\"award_key\"`\n\tEarnedCount    int64     `xorm:\"earned_count\"`\n\tIsBadgeDeleted int8      `xorm:\"is_badge_deleted\"`\n}\n\n// TableName badge_award table name\nfunc (BadgeAwardRecent) TableName() string {\n\treturn \"badge_award\"\n}\n"
  },
  {
    "path": "internal/entity/badge_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"time\"\n\n\t\"github.com/tidwall/gjson\"\n)\n\ntype BadgeLevel int\n\nconst (\n\tBadgeStatusActive   = 1\n\tBadgeStatusDeleted  = 10\n\tBadgeStatusInactive = 11\n\n\tBadgeLevelBronze BadgeLevel = 1\n\tBadgeLevelSilver BadgeLevel = 2\n\tBadgeLevelGold   BadgeLevel = 3\n\n\tBadgeSingleAward = 1\n\tBadgeMultiAward  = 2\n)\n\n// Badge badge\ntype Badge struct {\n\tID           string     `xorm:\"not null pk BIGINT(20) id\"`\n\tCreatedAt    time.Time  `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt    time.Time  `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tName         string     `xorm:\"not null default '' VARCHAR(256) name\"`\n\tIcon         string     `xorm:\"not null default '' VARCHAR(1024) icon\"`\n\tAwardCount   int        `xorm:\"not null default 0 INT(11) award_count\"`\n\tDescription  string     `xorm:\"not null MEDIUMTEXT description\"`\n\tStatus       int8       `xorm:\"not null default 1 INT(11) status\"`\n\tBadgeGroupID int64      `xorm:\"not null default 0 BIGINT(20) badge_group_id\"`\n\tLevel        BadgeLevel `xorm:\"not null default 1 TINYINT(4) level\"`\n\tSingle       int8       `xorm:\"not null default 1 TINYINT(4) single\"`\n\tCollect      string     `xorm:\"not null default '' VARCHAR(128) collect\"`\n\tHandler      string     `xorm:\"not null default '' VARCHAR(128) handler\"`\n\tParam        string     `xorm:\"not null TEXT param\"`\n}\n\n// TableName badge table name\nfunc (b *Badge) TableName() string {\n\treturn \"badge\"\n}\n\nfunc (b *Badge) GetIntParam(key string) int64 {\n\treturn gjson.Get(b.Param, key).Int()\n}\n\nfunc (b *Badge) GetStringParam(key string) string {\n\treturn gjson.Get(b.Param, key).String()\n}\n"
  },
  {
    "path": "internal/entity/badge_group_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// BadgeGroup badge_group\ntype BadgeGroup struct {\n\tID        string    `json:\"id\" xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tName      string    `json:\"name\" xorm:\"not null default '' VARCHAR(256) name\"`\n\tCreatedAt time.Time `json:\"created_at\" xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `json:\"updated_at\" xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n}\n\n// TableName badge_group table name\nfunc (BadgeGroup) TableName() string {\n\treturn \"badge_group\"\n}\n"
  },
  {
    "path": "internal/entity/captcha_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nconst (\n\tCaptchaActionEmail            = \"email\"\n\tCaptchaActionPassword         = \"password\"\n\tCaptchaActionEditUserinfo     = \"edit_userinfo\"\n\tCaptchaActionQuestion         = \"question\"\n\tCaptchaActionAnswer           = \"answer\"\n\tCaptchaActionComment          = \"comment\"\n\tCaptchaActionEdit             = \"edit\"\n\tCaptchaActionInvitationAnswer = \"invitation_answer\"\n\tCaptchaActionSearch           = \"search\"\n\tCaptchaActionReport           = \"report\"\n\tCaptchaActionDelete           = \"delete\"\n\tCaptchaActionVote             = \"vote\"\n)\n\ntype ActionRecordInfo struct {\n\tLastTime int64  `json:\"last_time\"`\n\tNum      int    `json:\"num\"`\n\tConfig   string `json:\"config\"`\n}\n"
  },
  {
    "path": "internal/entity/collection_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// Collection collection\ntype Collection struct {\n\tID                    string    `xorm:\"not null pk default 0 BIGINT(20) id\"`\n\tCreatedAt             time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt             time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tUserID                string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tObjectID              string    `xorm:\"not null default 0 BIGINT(20) object_id\"`\n\tUserCollectionGroupID string    `xorm:\"not null default 0 BIGINT(20) user_collection_group_id\"`\n}\n\ntype CollectionSearch struct {\n\tCollection\n\tPage     int `json:\"page\" form:\"page\"`           // Query number of pages\n\tPageSize int `json:\"page_size\" form:\"page_size\"` // Search page size\n}\n\n// TableName collection table name\nfunc (Collection) TableName() string {\n\treturn \"collection\"\n}\n"
  },
  {
    "path": "internal/entity/collection_group_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// CollectionGroup collection group\ntype CollectionGroup struct {\n\tID           string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt    time.Time `xorm:\"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt    time.Time `xorm:\"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at\"`\n\tUserID       string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tName         string    `xorm:\"not null default '' VARCHAR(50) name\"`\n\tDefaultGroup int       `xorm:\"not null default 1 INT(11) default_group\"`\n}\n\n// TableName collection group table name\nfunc (CollectionGroup) TableName() string {\n\treturn \"collection_group\"\n}\n"
  },
  {
    "path": "internal/entity/comment_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/pkg/converter\"\n)\n\nconst (\n\tCommentStatusAvailable = 1\n\tCommentStatusDeleted   = 10\n\tCommentStatusPending   = 11\n)\n\n// Comment comment\ntype Comment struct {\n\tID             string        `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time     `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time     `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID         string        `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tReplyUserID    sql.NullInt64 `xorm:\"BIGINT(20) reply_user_id\"`\n\tReplyCommentID sql.NullInt64 `xorm:\"BIGINT(20) reply_comment_id\"`\n\tObjectID       string        `xorm:\"not null default 0 BIGINT(20) INDEX object_id\"`\n\tQuestionID     string        `xorm:\"not null default 0 BIGINT(20) question_id\"`\n\tVoteCount      int           `xorm:\"not null default 0 INT(11) vote_count\"`\n\tStatus         int           `xorm:\"not null default 0 TINYINT(4) status\"`\n\tOriginalText   string        `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText     string        `xorm:\"not null MEDIUMTEXT parsed_text\"`\n}\n\n// TableName comment table name\nfunc (c *Comment) TableName() string {\n\treturn \"comment\"\n}\n\n// GetReplyUserID get reply user id\nfunc (c *Comment) GetReplyUserID() string {\n\tif c.ReplyUserID.Valid {\n\t\treturn fmt.Sprintf(\"%d\", c.ReplyUserID.Int64)\n\t}\n\treturn \"\"\n}\n\n// GetReplyCommentID get reply comment id\nfunc (c *Comment) GetReplyCommentID() string {\n\tif c.ReplyCommentID.Valid {\n\t\treturn fmt.Sprintf(\"%d\", c.ReplyCommentID.Int64)\n\t}\n\treturn \"\"\n}\n\n// SetReplyUserID set reply user id\nfunc (c *Comment) SetReplyUserID(str string) {\n\tif len(str) > 0 {\n\t\tc.ReplyUserID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true}\n\t} else {\n\t\tc.ReplyUserID = sql.NullInt64{Valid: false}\n\t}\n}\n\n// SetReplyCommentID set reply comment id\nfunc (c *Comment) SetReplyCommentID(str string) {\n\tif len(str) > 0 {\n\t\tc.ReplyCommentID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true}\n\t} else {\n\t\tc.ReplyCommentID = sql.NullInt64{Valid: false}\n\t}\n}\n\n// GetMentionUsernameList get mention username list\nfunc (c *Comment) GetMentionUsernameList() []string {\n\treturn converter.GetMentionUsernameList(c.OriginalText)\n}\n"
  },
  {
    "path": "internal/entity/config_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/pkg/converter\"\n)\n\n// Config config\ntype Config struct {\n\tID    int    `xorm:\"not null pk autoincr INT(11) id\"`\n\tKey   string `xorm:\"unique VARCHAR(128) key\"`\n\tValue string `xorm:\"TEXT value\"`\n}\n\n// TableName config table name\nfunc (c *Config) TableName() string {\n\treturn \"config\"\n}\n\nfunc (c *Config) BuildByJSON(data []byte) {\n\tcf := &Config{}\n\t_ = json.Unmarshal(data, cf)\n\tc.ID = cf.ID\n\tc.Key = cf.Key\n\tc.Value = cf.Value\n}\n\nfunc (c *Config) JsonString() string {\n\tdata, _ := json.Marshal(c)\n\treturn string(data)\n}\n\n// GetIntValue get int value\nfunc (c *Config) GetIntValue() int {\n\tif len(c.Value) == 0 {\n\t\tlog.Warnf(\"config value is empty, key: %s, value: %s\", c.Key, c.Value)\n\t}\n\treturn converter.StringToInt(c.Value)\n}\n\n// GetArrayStringValue get array string value\nfunc (c *Config) GetArrayStringValue() []string {\n\tvar arr []string\n\t_ = json.Unmarshal([]byte(c.Value), &arr)\n\treturn arr\n}\n\n// GetByteValue get byte value\nfunc (c *Config) GetByteValue() []byte {\n\treturn []byte(c.Value)\n}\n"
  },
  {
    "path": "internal/entity/file_record_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tFileRecordStatusAvailable = 1\n\tFileRecordStatusDeleted   = 10\n)\n\n// FileRecord file record\ntype FileRecord struct {\n\tID        int       `xorm:\"not null pk autoincr INT(10) id\"`\n\tCreatedAt time.Time `xorm:\"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at\"`\n\tUserID    string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tFilePath  string    `xorm:\"not null VARCHAR(256) file_path\"`\n\tFileURL   string    `xorm:\"not null VARCHAR(1024) file_url\"`\n\tObjectID  string    `xorm:\"not null default 0 INDEX BIGINT(20) object_id\"`\n\tSource    string    `xorm:\"not null VARCHAR(128) source\"`\n\tStatus    int       `xorm:\"not null default 0 TINYINT(4) status\"`\n}\n\n// TableName file record table name\nfunc (FileRecord) TableName() string {\n\treturn \"file_record\"\n}\n"
  },
  {
    "path": "internal/entity/meta_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tQuestionEditSummaryKey = \"question.edit.summary\"\n\tQuestionCloseReasonKey = \"question.close.reason\"\n\tAnswerEditSummaryKey   = \"answer.edit.summary\"\n\tTagEditSummaryKey      = \"tag.edit.summary\"\n\tObjectReactSummaryKey  = \"object.react.summary\"\n)\n\n// Meta meta\ntype Meta struct {\n\tID        int       `xorm:\"not null pk autoincr INT(10) id\"`\n\tCreatedAt time.Time `xorm:\"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at\"`\n\tObjectID  string    `xorm:\"not null default 0 INDEX BIGINT(20) object_id\"`\n\tKey       string    `xorm:\"not null VARCHAR(100) key\"`\n\tValue     string    `xorm:\"not null MEDIUMTEXT value\"`\n}\n\n// TableName meta table name\nfunc (Meta) TableName() string {\n\treturn \"meta\"\n}\n"
  },
  {
    "path": "internal/entity/notification_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// Notification notification\ntype Notification struct {\n\tID        string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"TIMESTAMP updated_at\"`\n\tUserID    string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tObjectID  string    `xorm:\"not null default 0 INDEX BIGINT(20) object_id\"`\n\tContent   string    `xorm:\"not null TEXT content\"`\n\tType      int       `xorm:\"not null default 0 INT(11) type\"`\n\tMsgType   int       `xorm:\"not null default 0 INT(11) msg_type\"`\n\tIsRead    int       `xorm:\"not null default 1 INT(11) is_read\"`\n\tStatus    int       `xorm:\"not null default 1 INT(11) status\"`\n}\n\n// TableName notification table name\nfunc (Notification) TableName() string {\n\treturn \"notification\"\n}\n"
  },
  {
    "path": "internal/entity/plugin_config_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\n// PluginConfig plugin config\ntype PluginConfig struct {\n\tID             int    `xorm:\"not null pk autoincr INT(11) id\"`\n\tPluginSlugName string `xorm:\"unique VARCHAR(128) plugin_slug_name\"`\n\tValue          string `xorm:\"TEXT value\"`\n}\n\n// TableName config table name\nfunc (PluginConfig) TableName() string {\n\treturn \"plugin_config\"\n}\n"
  },
  {
    "path": "internal/entity/plugin_kv_storage_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\ntype PluginKVStorage struct {\n\tID             int    `xorm:\"not null pk autoincr INT(11) id\"`\n\tPluginSlugName string `xorm:\"not null VARCHAR(128) UNIQUE(uk_psg) plugin_slug_name\"`\n\tGroup          string `xorm:\"not null VARCHAR(128) UNIQUE(uk_psg) 'group'\"`\n\tKey            string `xorm:\"not null VARCHAR(128) UNIQUE(uk_psg) 'key'\"`\n\tValue          string `xorm:\"not null TEXT value\"`\n}\n\nfunc (PluginKVStorage) TableName() string {\n\treturn \"plugin_kv_storage\"\n}\n"
  },
  {
    "path": "internal/entity/plugin_user_config_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\n// PluginUserConfig plugin config\ntype PluginUserConfig struct {\n\tID             int    `xorm:\"not null pk autoincr INT(11) id\"`\n\tUserID         string `xorm:\"not null default 0 BIGINT(20) UNIQUE(uk_up) user_id\"`\n\tPluginSlugName string `xorm:\"VARCHAR(128) UNIQUE(uk_up) plugin_slug_name\"`\n\tValue          string `xorm:\"TEXT value\"`\n}\n\n// TableName config table name\nfunc (PluginUserConfig) TableName() string {\n\treturn \"plugin_user_config\"\n}\n"
  },
  {
    "path": "internal/entity/power_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// Power power\ntype Power struct {\n\tID          int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt   time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt   time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tName        string    `xorm:\"not null default '' VARCHAR(50) name\"`\n\tPowerType   string    `xorm:\"not null default '' VARCHAR(100) power_type\"`\n\tDescription string    `xorm:\"not null default '' VARCHAR(200) description\"`\n}\n\n// TableName power table name\nfunc (Power) TableName() string {\n\treturn \"power\"\n}\n"
  },
  {
    "path": "internal/entity/question_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"time\"\n)\n\nconst (\n\tQuestionStatusAvailable = 1\n\tQuestionStatusClosed    = 2\n\tQuestionStatusDeleted   = 10\n\tQuestionStatusPending   = 11\n\tQuestionUnPin           = 1\n\tQuestionPin             = 2\n\tQuestionShow            = 1\n\tQuestionHide            = 2\n)\n\nvar AdminQuestionSearchStatus = map[string]int{\n\t\"available\": QuestionStatusAvailable,\n\t\"closed\":    QuestionStatusClosed,\n\t\"deleted\":   QuestionStatusDeleted,\n\t\"pending\":   QuestionStatusPending,\n}\n\nvar AdminQuestionSearchStatusIntToString = map[int]string{\n\tQuestionStatusAvailable: \"available\",\n\tQuestionStatusClosed:    \"closed\",\n\tQuestionStatusDeleted:   \"deleted\",\n\tQuestionStatusPending:   \"pending\",\n}\n\n// Question question\ntype Question struct {\n\tID               string    `xorm:\"not null pk BIGINT(20) id\"`\n\tCreatedAt        time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt        time.Time `xorm:\"updated_at TIMESTAMP\"`\n\tUserID           string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tInviteUserID     string    `xorm:\"TEXT invite_user_id\"`\n\tLastEditUserID   string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\tTitle            string    `xorm:\"not null default '' VARCHAR(150) title\"`\n\tOriginalText     string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText       string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\tPin              int       `xorm:\"not null default 1 INT(11) pin\"`\n\tShow             int       `xorm:\"not null default 1 INT(11) show\"`\n\tStatus           int       `xorm:\"not null default 1 INT(11) status\"`\n\tViewCount        int       `xorm:\"not null default 0 INT(11) view_count\"`\n\tUniqueViewCount  int       `xorm:\"not null default 0 INT(11) unique_view_count\"`\n\tVoteCount        int       `xorm:\"not null default 0 INT(11) vote_count\"`\n\tAnswerCount      int       `xorm:\"not null default 0 INT(11) answer_count\"`\n\tHotScore         int       `xorm:\"not null default 0 INT(11) hot_score\"`\n\tCollectionCount  int       `xorm:\"not null default 0 INT(11) collection_count\"`\n\tFollowCount      int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\tAcceptedAnswerID string    `xorm:\"not null default 0 BIGINT(20) accepted_answer_id\"`\n\tLastAnswerID     string    `xorm:\"not null default 0 BIGINT(20) last_answer_id\"`\n\tPostUpdateTime   time.Time `xorm:\"post_update_time TIMESTAMP\"`\n\tRevisionID       string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n\tLinkedCount      int       `xorm:\"not null default 0 INT(11) linked_count\"`\n}\n\n// TableName question table name\nfunc (Question) TableName() string {\n\treturn \"question\"\n}\n\n// QuestionWithTagsRevision question\ntype QuestionWithTagsRevision struct {\n\tQuestion\n\tTags []*TagSimpleInfoForRevision `json:\"tags\"`\n}\n\n// TagSimpleInfoForRevision tag simple info for revision\ntype TagSimpleInfoForRevision struct {\n\tID              string `xorm:\"not null pk comment('tag_id') BIGINT(20) id\"`\n\tMainTagID       int64  `xorm:\"not null default 0 BIGINT(20) main_tag_id\"`\n\tMainTagSlugName string `xorm:\"not null default '' VARCHAR(35) main_tag_slug_name\"`\n\tSlugName        string `xorm:\"not null default '' unique VARCHAR(35) slug_name\"`\n\tDisplayName     string `xorm:\"not null default '' VARCHAR(35) display_name\"`\n\tRecommend       bool   `xorm:\"not null default false BOOL recommend\"`\n\tReserved        bool   `xorm:\"not null default false BOOL reserved\"`\n\tRevisionID      string `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n}\n"
  },
  {
    "path": "internal/entity/question_link_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"time\"\n)\n\nconst (\n\tQuestionLinkStatusAvailable = 1\n\tQuestionLinkStatusDeleted   = 2\n)\n\ntype QuestionLink struct {\n\tID             string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated_at TIMESTAMP\"`\n\tFromQuestionID string    `xorm:\"not null default 0 BIGINT(20) index from_question_id\"`\n\tFromAnswerID   string    `xorm:\"BIGINT(20) from_answer_id\"`\n\tToQuestionID   string    `xorm:\"not null default 0 BIGINT(20) index to_question_id\"`\n\tToAnswerID     string    `xorm:\"BIGINT(20) to_answer_id\"`\n\tStatus         int       `xorm:\"not null default 1 INT(11) status\"`\n}\n\nfunc (QuestionLink) TableName() string {\n\treturn \"question_link\"\n}\n"
  },
  {
    "path": "internal/entity/report_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tReportStatusPending   = 1\n\tReportStatusCompleted = 2\n\tReportStatusIgnore    = 3\n\tReportStatusDeleted   = 10\n)\n\nvar (\n\tReportStatus = map[string]int{\n\t\t\"pending\":   ReportStatusPending,\n\t\t\"completed\": ReportStatusCompleted,\n\t\t\"deleted\":   ReportStatusDeleted,\n\t}\n)\n\n// Report report\ntype Report struct {\n\tID             string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID         string    `xorm:\"not null BIGINT(20) user_id\"`\n\tObjectID       string    `xorm:\"not null BIGINT(20) object_id\"`\n\tReportedUserID string    `xorm:\"not null default 0 BIGINT(20) reported_user_id\"`\n\tObjectType     int       `xorm:\"not null default 0 INT(11) object_type\"`\n\tReportType     int       `xorm:\"not null default 0 INT(11) report_type\"`\n\tContent        string    `xorm:\"not null TEXT content\"`\n\tFlaggedType    int       `xorm:\"not null default 0 INT(11) flagged_type\"`\n\tFlaggedContent string    `xorm:\"TEXT flagged_content\"`\n\tStatus         int       `xorm:\"not null default 1 INT(11) status\"`\n}\n\n// TableName report table name\nfunc (Report) TableName() string {\n\treturn \"report\"\n}\n"
  },
  {
    "path": "internal/entity/review_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tReviewStatusPending  = 1\n\tReviewStatusApproved = 2\n\tReviewStatusRejected = 3\n)\n\n// Review review\ntype Review struct {\n\tID             int       `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID         string    `xorm:\"not null BIGINT(20) user_id\"`\n\tObjectID       string    `xorm:\"not null BIGINT(20) object_id\"`\n\tObjectType     int       `xorm:\"not null default 0 INT(11) object_type\"`\n\tReviewerUserID string    `xorm:\"not null default 0 BIGINT(20) reviewer_user_id\"`\n\tSubmitter      string    `xorm:\"not null default '' VARCHAR(100) submitter\"`\n\tReason         string    `xorm:\"not null TEXT reason\"`\n\tStatus         int       `xorm:\"not null default 0 INT(11) status\"`\n}\n\n// TableName review table name\nfunc (Review) TableName() string {\n\treturn \"review\"\n}\n"
  },
  {
    "path": "internal/entity/revision_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport (\n\t\"time\"\n)\n\nconst (\n\t// RevisionNormalStatus this revision is normal\n\tRevisionNormalStatus = 0\n\t// RevisionUnreviewedStatus this revision is unreviewed\n\tRevisionUnreviewedStatus = 1\n\t// RevisionReviewPassStatus this revision is reviewed and approved by operator\n\tRevisionReviewPassStatus = 2\n\t// RevisionReviewRejectStatus this revision is reviewed and rejected by operator\n\tRevisionReviewRejectStatus = 3\n)\n\n// Revision revision\ntype Revision struct {\n\tID           string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt    time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt    time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID       string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tObjectType   int       `xorm:\"not null default 0 INT(11) object_type\"`\n\tObjectID     string    `xorm:\"not null default 0 BIGINT(20) INDEX object_id\"`\n\tTitle        string    `xorm:\"not null default '' VARCHAR(255) title\"`\n\tContent      string    `xorm:\"not null MEDIUMTEXT content\"`\n\tLog          string    `xorm:\"VARCHAR(255) log\"`\n\tStatus       int       `xorm:\"not null default 1 INT(11) status\"`\n\tReviewUserID int64     `xorm:\"not null default 0 BIGINT(20) review_user_id\"`\n}\n\n// TableName revision table name\nfunc (Revision) TableName() string {\n\treturn \"revision\"\n}\n"
  },
  {
    "path": "internal/entity/role_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// Role role\ntype Role struct {\n\tID          int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt   time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt   time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tName        string    `xorm:\"not null default '' VARCHAR(50) name\"`\n\tDescription string    `xorm:\"not null default '' VARCHAR(200) description\"`\n}\n\n// TableName user table name\nfunc (Role) TableName() string {\n\treturn \"role\"\n}\n"
  },
  {
    "path": "internal/entity/role_power_rel_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// RolePowerRel role power rel\ntype RolePowerRel struct {\n\tID        int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tRoleID    int       `xorm:\"not null default 0 INT(11) role_id\"`\n\tPowerType string    `xorm:\"not null default '' VARCHAR(200) power_type\"`\n}\n\n// TableName role power rel table name\nfunc (RolePowerRel) TableName() string {\n\treturn \"role_power_rel\"\n}\n"
  },
  {
    "path": "internal/entity/site_info.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// SiteInfo site information setting\ntype SiteInfo struct {\n\tID        string    `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tType      string    `xorm:\"not null VARCHAR(64) type\"`\n\tContent   string    `xorm:\"not null MEDIUMTEXT content\"`\n\tStatus    int       `xorm:\"not null default 1 INT(11) status\"`\n}\n\n// TableName table name\nfunc (*SiteInfo) TableName() string {\n\treturn \"site_info\"\n}\n"
  },
  {
    "path": "internal/entity/tag_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tTagStatusAvailable = 1\n\tTagStatusDeleted   = 10\n)\n\nvar TagStatusDisplayMapping = map[int]string{\n\tTagStatusAvailable: \"available\",\n\tTagStatusDeleted:   \"deleted\",\n}\n\n// Tag tag\ntype Tag struct {\n\tID              string    `xorm:\"not null pk comment('tag_id') BIGINT(20) id\"`\n\tCreatedAt       time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt       time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tMainTagID       int64     `xorm:\"not null default 0 BIGINT(20) main_tag_id\"`\n\tMainTagSlugName string    `xorm:\"not null default '' VARCHAR(35) main_tag_slug_name\"`\n\tSlugName        string    `xorm:\"not null default '' unique VARCHAR(35) slug_name\"`\n\tDisplayName     string    `xorm:\"not null default '' VARCHAR(35) display_name\"`\n\tOriginalText    string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText      string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\tFollowCount     int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\tQuestionCount   int       `xorm:\"not null default 0 INT(11) question_count\"`\n\tStatus          int       `xorm:\"not null default 1 INT(11) status\"`\n\tRecommend       bool      `xorm:\"not null default false BOOL recommend\"`\n\tReserved        bool      `xorm:\"not null default false BOOL reserved\"`\n\tRevisionID      string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n\tUserID          string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n}\n\n// TableName tag table name\nfunc (Tag) TableName() string {\n\treturn \"tag\"\n}\n"
  },
  {
    "path": "internal/entity/tag_rel_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tTagRelStatusAvailable = 1\n\tTagRelStatusHide      = 2\n\tTagRelStatusDeleted   = 10\n)\n\n// TagRel tag relation\ntype TagRel struct {\n\tID        int64     `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tObjectID  string    `xorm:\"not null INDEX UNIQUE(s) BIGINT(20) object_id\"`\n\tTagID     string    `xorm:\"not null INDEX UNIQUE(s) BIGINT(20) tag_id\"`\n\tStatus    int       `xorm:\"not null default 1 INT(11) status\"`\n}\n\n// TableName tag list table name\nfunc (TagRel) TableName() string {\n\treturn \"tag_rel\"\n}\n"
  },
  {
    "path": "internal/entity/uniqid_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\n// Uniqid uniqid\ntype Uniqid struct {\n\tID         int64 `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tUniqidType int   `xorm:\"not null default 0 INT(11) uniqid_type\"`\n}\n\n// TableName uniqid table name\nfunc (Uniqid) TableName() string {\n\treturn \"uniqid\"\n}\n"
  },
  {
    "path": "internal/entity/user_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\nconst (\n\tUserStatusAvailable = 1\n\tUserStatusSuspended = 9\n\tUserStatusDeleted   = 10\n)\n\nconst (\n\tEmailStatusAvailable    = 1\n\tEmailStatusToBeVerified = 2\n)\n\nconst (\n\tUserAdminFlag = 1\n)\n\n// PermanentSuspensionTime is a fixed time representing permanent suspension (2099-12-31 23:59:59)\nvar PermanentSuspensionTime = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)\n\n// User user\ntype User struct {\n\tID             string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt      time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt      time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tSuspendedAt    time.Time `xorm:\"TIMESTAMP suspended_at\"`\n\tSuspendedUntil time.Time `xorm:\"DATETIME suspended_until\"`\n\tDeletedAt      time.Time `xorm:\"TIMESTAMP deleted_at\"`\n\tLastLoginDate  time.Time `xorm:\"TIMESTAMP last_login_date\"`\n\tUsername       string    `xorm:\"not null default '' VARCHAR(50) UNIQUE username\"`\n\tPass           string    `xorm:\"not null default '' VARCHAR(255) pass\"`\n\tEMail          string    `xorm:\"not null VARCHAR(100) e_mail\"`\n\tMailStatus     int       `xorm:\"not null default 2 TINYINT(4) mail_status\"`\n\tNoticeStatus   int       `xorm:\"not null default 2 INT(11) notice_status\"`\n\tFollowCount    int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\tAnswerCount    int       `xorm:\"not null default 0 INT(11) answer_count\"`\n\tQuestionCount  int       `xorm:\"not null default 0 INT(11) question_count\"`\n\tRank           int       `xorm:\"not null default 0 INT(11) rank\"`\n\tStatus         int       `xorm:\"not null default 1 INT(11) status\"`\n\tAuthorityGroup int       `xorm:\"not null default 1 INT(11) authority_group\"`\n\tDisplayName    string    `xorm:\"not null default '' VARCHAR(30) display_name\"`\n\tAvatar         string    `xorm:\"not null default '' VARCHAR(2048) avatar\"`\n\tMobile         string    `xorm:\"not null VARCHAR(20) mobile\"`\n\tBio            string    `xorm:\"not null TEXT bio\"`\n\tBioHTML        string    `xorm:\"not null TEXT bio_html\"`\n\tWebsite        string    `xorm:\"not null default '' VARCHAR(255) website\"`\n\tLocation       string    `xorm:\"not null default '' VARCHAR(100) location\"`\n\tIPInfo         string    `xorm:\"not null default '' VARCHAR(255) ip_info\"`\n\tIsAdmin        bool      `xorm:\"not null default false BOOL is_admin\"`\n\tLanguage       string    `xorm:\"not null default '' VARCHAR(100) language\"`\n\tColorScheme    string    `xorm:\"not null default '' VARCHAR(100) color_scheme\"`\n}\n\n// TableName user table name\nfunc (User) TableName() string {\n\treturn \"user\"\n}\n\ntype UserSearch struct {\n\tUser\n\tPage     int `json:\"page\" form:\"page\"`           // Query number of pages\n\tPageSize int `json:\"page_size\" form:\"page_size\"` // Search page size\n}\n"
  },
  {
    "path": "internal/entity/user_external_login_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// UserExternalLogin user external login\ntype UserExternalLogin struct {\n\tID         int64     `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt  time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt  time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID     string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tProvider   string    `xorm:\"not null default '' VARCHAR(100) provider\"`\n\tExternalID string    `xorm:\"not null default '' VARCHAR(128) external_id\"`\n\tMetaInfo   string    `xorm:\"TEXT meta_info\"`\n}\n\n// TableName  table name\nfunc (UserExternalLogin) TableName() string {\n\treturn \"user_external_login\"\n}\n"
  },
  {
    "path": "internal/entity/user_notification_config_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// UserNotificationConfig user notification config\ntype UserNotificationConfig struct {\n\tID        string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID    string    `xorm:\"not null default 0 INDEX UNIQUE(uk_us) BIGINT(20) INDEX user_id\"`\n\tSource    string    `xorm:\"not null default '' INDEX UNIQUE(uk_us) VARCHAR(64) source\"`\n\tChannels  string    `xorm:\"not null TEXT channels\"`\n\tEnabled   bool      `xorm:\"not null default false BOOL enabled\"`\n}\n\n// TableName notification table name\nfunc (UserNotificationConfig) TableName() string {\n\treturn \"user_notification_config\"\n}\n"
  },
  {
    "path": "internal/entity/user_role_rel_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\nimport \"time\"\n\n// UserRoleRel role\ntype UserRoleRel struct {\n\tID        int       `xorm:\"not null pk autoincr INT(11) id\"`\n\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID    string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tRoleID    int       `xorm:\"not null default 0 INT(11) role_id\"`\n}\n\n// TableName user role rel table name\nfunc (UserRoleRel) TableName() string {\n\treturn \"user_role_rel\"\n}\n"
  },
  {
    "path": "internal/entity/version_entity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage entity\n\n// Version version\ntype Version struct {\n\tID            int   `xorm:\"not null pk autoincr INT(11) id\"`\n\tVersionNumber int64 `xorm:\"not null default 0 INT(11) version_number\"`\n}\n\n// TableName config table name\nfunc (Version) TableName() string {\n\treturn \"version\"\n}\n"
  },
  {
    "path": "internal/install/install_controller.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage install\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/apache/answer/configs\"\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/cli\"\n\t\"github.com/apache/answer/internal/migrations\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// LangOptions get installation language options\n// @Summary get installation language options\n// @Description get installation language options\n// @Tags Lang\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=[]translator.LangOption}\n// @Router /installation/language/options [get]\nfunc LangOptions(ctx *gin.Context) {\n\thandler.HandleResponse(ctx, nil, translator.LanguageOptions)\n}\n\n// GetLangMapping get installation language config mapping\n// @Summary get installation language config mapping\n// @Description get installation language config mapping\n// @Tags Lang\n// @Param lang query string true \"installation language\"\n// @Produce json\n// @Success 200 {object} handler.RespBody{}\n// @Router /installation/language/config [get]\nfunc GetLangMapping(ctx *gin.Context) {\n\tt, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})\n\tif err != nil {\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tlang := ctx.Query(\"lang\")\n\ttrData, _ := t.Dump(i18n.Language(lang))\n\tvar resp map[string]any\n\t_ = json.Unmarshal(trData, &resp)\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// CheckConfigFileAndRedirectToInstallPage if config file not exist try to redirect to install page\n// @Summary if config file not exist try to redirect to install page\n// @Description if config file not exist try to redirect to install page\n// @Tags installation\n// @Accept json\n// @Produce json\n// @Router / [get]\nfunc CheckConfigFileAndRedirectToInstallPage(ctx *gin.Context) {\n\tif cli.CheckConfigFile(confPath) {\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t} else {\n\t\tctx.Redirect(http.StatusFound, \"/install\")\n\t}\n}\n\n// CheckConfigFile check config file if exist when installation\n// @Summary check config file if exist when installation\n// @Description check config file if exist when installation\n// @Tags installation\n// @Accept json\n// @Produce json\n// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}\n// @Router /installation/config-file/check [post]\nfunc CheckConfigFile(ctx *gin.Context) {\n\tresp := &CheckConfigFileResp{}\n\tresp.ConfigFileExist = cli.CheckConfigFile(confPath)\n\tif !resp.ConfigFileExist {\n\t\thandler.HandleResponse(ctx, nil, resp)\n\t\treturn\n\t}\n\tallConfig, err := conf.ReadConfig(confPath)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\terr = errors.BadRequest(reason.ReadConfigFailed)\n\t\thandler.HandleResponse(ctx, err, nil)\n\t\treturn\n\t}\n\tresp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database)\n\tif resp.DBConnectionSuccess {\n\t\tresp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database)\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// CheckDatabase check database if exist when installation\n// @Summary check database if exist when installation\n// @Description check database if exist when installation\n// @Tags installation\n// @Accept json\n// @Produce json\n// @Param data body install.CheckDatabaseReq  true \"CheckDatabaseReq\"\n// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}\n// @Router /installation/db/check [post]\nfunc CheckDatabase(ctx *gin.Context) {\n\treq := &CheckDatabaseReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\tresp := &CheckDatabaseResp{}\n\tdataConf := &data.Database{\n\t\tDriver:     req.DbType,\n\t\tConnection: req.GetConnection(),\n\t}\n\tresp.ConnectionSuccess = cli.CheckDBConnection(dataConf)\n\tif !resp.ConnectionSuccess {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, resp)\n}\n\n// InitEnvironment init environment\n// @Summary init environment\n// @Description init environment\n// @Tags installation\n// @Accept json\n// @Produce json\n// @Param data body install.CheckDatabaseReq  true \"CheckDatabaseReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /installation/init [post]\nfunc InitEnvironment(ctx *gin.Context) {\n\treq := &CheckDatabaseReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\n\t// check config file if exist\n\tif cli.CheckConfigFile(confPath) {\n\t\tlog.Debug(\"config file already exists\")\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\n\tif err := cli.InstallConfigFile(confPath); err != nil {\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{\n\t\t\tSuccess:            false,\n\t\t\tCreateConfigFailed: true,\n\t\t\tDefaultConfig:      string(configs.Config),\n\t\t\tErrType:            schema.ErrTypeAlert.ErrType,\n\t\t})\n\t\treturn\n\t}\n\n\tc, err := conf.ReadConfig(confPath)\n\tif err != nil {\n\t\tlog.Errorf(\"read config failed %s\", err)\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)\n\t\treturn\n\t}\n\tc.Data.Database.Driver = req.DbType\n\tc.Data.Database.Connection = req.GetConnection()\n\tc.Data.Cache.FilePath = filepath.Join(path.CacheDir, path.DefaultCacheFileName)\n\tc.I18n.BundleDir = path.I18nPath\n\tc.ServiceConfig.UploadPath = path.UploadFilePath\n\n\tif err := conf.RewriteConfig(confPath, c); err != nil {\n\t\tlog.Errorf(\"rewrite config failed %s\", err)\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)\n\t\treturn\n\t}\n\thandler.HandleResponse(ctx, nil, nil)\n}\n\n// InitBaseInfo init base info\n// @Summary init base info\n// @Description init base info\n// @Tags installation\n// @Accept json\n// @Produce json\n// @Param data body install.InitBaseInfoReq  true \"InitBaseInfoReq\"\n// @Success 200 {object} handler.RespBody{}\n// @Router /installation/base-info [post]\nfunc InitBaseInfo(ctx *gin.Context) {\n\treq := &InitBaseInfoReq{}\n\tif handler.BindAndCheck(ctx, req) {\n\t\treturn\n\t}\n\treq.FormatSiteUrl()\n\n\tc, err := conf.ReadConfig(confPath)\n\tif err != nil {\n\t\tlog.Errorf(\"read config failed %s\", err)\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)\n\t\treturn\n\t}\n\n\tif cli.CheckDBTableExist(c.Data.Database) {\n\t\tlog.Warn(\"database is already initialized\")\n\t\thandler.HandleResponse(ctx, nil, nil)\n\t\treturn\n\t}\n\n\tengine, err := data.NewDB(false, c.Data.Database)\n\tif err != nil {\n\t\tlog.Errorf(\"init database failed %s\", err)\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil)\n\t}\n\n\tinputData := &migrations.InitNeedUserInputData{}\n\t_ = copier.Copy(inputData, req)\n\tif err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil {\n\t\tlog.Error(\"init database error: \", err.Error())\n\t\thandler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert)\n\t\treturn\n\t}\n\n\thandler.HandleResponse(ctx, nil, nil)\n\tgo func() {\n\t\ttime.Sleep(1 * time.Second)\n\t\tos.Exit(0)\n\t}()\n}\n"
  },
  {
    "path": "internal/install/install_from_env.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage install\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/tidwall/gjson\"\n)\n\ntype Env struct {\n\tAutoInstall string `json:\"auto_install\"`\n\tDbType      string `json:\"db_type\"`\n\tDbUsername  string `json:\"db_username\"`\n\tDbPassword  string `json:\"db_password\"`\n\tDbHost      string `json:\"db_host\"`\n\tDbName      string `json:\"db_name\"`\n\tDbFile      string `json:\"db_file\"`\n\tLanguage    string `json:\"lang\"`\n\n\tSiteName               string `json:\"site_name\"`\n\tSiteURL                string `json:\"site_url\"`\n\tContactEmail           string `json:\"contact_email\"`\n\tAdminName              string `json:\"name\"`\n\tAdminPassword          string `json:\"password\"`\n\tAdminEmail             string `json:\"email\"`\n\tLoginRequired          bool   `json:\"login_required\"`\n\tExternalContentDisplay string `json:\"external_content_display\"`\n}\n\nfunc TryToInstallByEnv() (installByEnv bool, err error) {\n\tenv := loadEnv()\n\tif len(env.AutoInstall) == 0 {\n\t\treturn false, nil\n\t}\n\tfmt.Println(\"[auto-install] try to install by environment variable\")\n\treturn true, initByEnv(env)\n}\n\nfunc loadEnv() (env *Env) {\n\treturn &Env{\n\t\tAutoInstall:            os.Getenv(\"AUTO_INSTALL\"),\n\t\tDbType:                 os.Getenv(\"DB_TYPE\"),\n\t\tDbUsername:             os.Getenv(\"DB_USERNAME\"),\n\t\tDbPassword:             os.Getenv(\"DB_PASSWORD\"),\n\t\tDbHost:                 os.Getenv(\"DB_HOST\"),\n\t\tDbName:                 os.Getenv(\"DB_NAME\"),\n\t\tDbFile:                 os.Getenv(\"DB_FILE\"),\n\t\tLanguage:               os.Getenv(\"LANGUAGE\"),\n\t\tSiteName:               os.Getenv(\"SITE_NAME\"),\n\t\tSiteURL:                os.Getenv(\"SITE_URL\"),\n\t\tContactEmail:           os.Getenv(\"CONTACT_EMAIL\"),\n\t\tAdminName:              os.Getenv(\"ADMIN_NAME\"),\n\t\tAdminPassword:          os.Getenv(\"ADMIN_PASSWORD\"),\n\t\tAdminEmail:             os.Getenv(\"ADMIN_EMAIL\"),\n\t\tExternalContentDisplay: os.Getenv(\"EXTERNAL_CONTENT_DISPLAY\"),\n\t}\n}\n\nfunc initByEnv(env *Env) (err error) {\n\tgin.SetMode(gin.TestMode)\n\tif err = dbCheck(env); err != nil {\n\t\treturn err\n\t}\n\tif err = initConfigAndDb(env); err != nil {\n\t\treturn err\n\t}\n\tif err = initBaseInfo(env); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc dbCheck(env *Env) (err error) {\n\treq := &CheckDatabaseReq{\n\t\tDbType:     env.DbType,\n\t\tDbUsername: env.DbUsername,\n\t\tDbPassword: env.DbPassword,\n\t\tDbHost:     env.DbHost,\n\t\tDbName:     env.DbName,\n\t\tDbFile:     env.DbFile,\n\t}\n\treturn requestAPI(req, \"POST\", \"/installation/db/check\", CheckDatabase)\n}\n\nfunc initConfigAndDb(env *Env) (err error) {\n\treq := &CheckDatabaseReq{\n\t\tDbType:     env.DbType,\n\t\tDbUsername: env.DbUsername,\n\t\tDbPassword: env.DbPassword,\n\t\tDbHost:     env.DbHost,\n\t\tDbName:     env.DbName,\n\t\tDbFile:     env.DbFile,\n\t}\n\treturn requestAPI(req, \"POST\", \"/installation/init\", InitEnvironment)\n}\n\nfunc initBaseInfo(env *Env) (err error) {\n\treq := &InitBaseInfoReq{\n\t\tLanguage:               env.Language,\n\t\tSiteName:               env.SiteName,\n\t\tSiteURL:                env.SiteURL,\n\t\tContactEmail:           env.ContactEmail,\n\t\tAdminName:              env.AdminName,\n\t\tAdminPassword:          env.AdminPassword,\n\t\tAdminEmail:             env.AdminEmail,\n\t\tLoginRequired:          env.LoginRequired,\n\t\tExternalContentDisplay: env.ExternalContentDisplay,\n\t}\n\treturn requestAPI(req, \"POST\", \"/installation/base-info\", InitBaseInfo)\n}\n\nfunc requestAPI(req any, method, url string, handlerFunc gin.HandlerFunc) error {\n\tw := httptest.NewRecorder()\n\tc, _ := gin.CreateTestContext(w)\n\tbody, _ := json.Marshal(req)\n\tc.Request, _ = http.NewRequest(method, url, bytes.NewBuffer(body))\n\tif method == \"POST\" {\n\t\tc.Request.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\thandlerFunc(c)\n\tif w.Code != http.StatusOK {\n\t\treturn errors.New(gjson.Get(w.Body.String(), \"msg\").String())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/install/install_main.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage install\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/joho/godotenv\"\n)\n\nvar (\n\tport     string\n\tconfPath = \"\"\n)\n\nfunc init() {\n\t_ = godotenv.Load()\n\tport = os.Getenv(\"INSTALL_PORT\")\n}\n\nfunc Run(configPath string) {\n\tconfPath = configPath\n\t// initialize translator for return internationalization error when installing.\n\t_, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// try to install by env\n\tif installByEnv, err := TryToInstallByEnv(); installByEnv && err != nil {\n\t\tfmt.Printf(\"[auto-install] try to init by env fail: %v\\n\", err)\n\t}\n\n\tinstallServer := NewInstallHTTPServer()\n\tif len(port) == 0 {\n\t\tport = \"80\"\n\t}\n\tfmt.Printf(\"[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \\n\", port)\n\tif err = installServer.Run(\":\" + port); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "internal/install/install_req.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage install\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm/schemas\"\n)\n\n// CheckConfigFileResp check config file if exist or not response\ntype CheckConfigFileResp struct {\n\tConfigFileExist     bool `json:\"config_file_exist\"`\n\tDBConnectionSuccess bool `json:\"db_connection_success\"`\n\tDbTableExist        bool `json:\"db_table_exist\"`\n}\n\n// CheckDatabaseReq check database\ntype CheckDatabaseReq struct {\n\tDbType      string `validate:\"required,oneof=postgres sqlite3 mysql\" json:\"db_type\"`\n\tDbUsername  string `json:\"db_username\"`\n\tDbPassword  string `json:\"db_password\"`\n\tDbHost      string `json:\"db_host\"`\n\tDbName      string `json:\"db_name\"`\n\tDbFile      string `json:\"db_file\"`\n\tSsl         bool   `json:\"ssl_enabled\"`\n\tSslMode     string `json:\"ssl_mode\"`\n\tSslRootCert string `json:\"ssl_root_cert\"`\n\tSslKey      string `json:\"ssl_key\"`\n\tSslCert     string `json:\"ssl_cert\"`\n}\n\n// GetConnection get connection string\nfunc (r *CheckDatabaseReq) GetConnection() string {\n\tif r.DbType == string(schemas.SQLITE) {\n\t\treturn r.DbFile\n\t}\n\tif r.DbType == string(schemas.MYSQL) {\n\t\treturn fmt.Sprintf(\"%s:%s@tcp(%s)/%s\",\n\t\t\tr.DbUsername, r.DbPassword, r.DbHost, r.DbName)\n\t}\n\tif r.DbType == string(schemas.POSTGRES) {\n\t\thost, port := parsePgSQLHostPort(r.DbHost)\n\t\tswitch {\n\t\tcase !r.Ssl:\n\t\t\treturn fmt.Sprintf(\"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable\",\n\t\t\t\thost, port, r.DbUsername, r.DbPassword, r.DbName)\n\t\tcase r.SslMode == \"require\":\n\t\t\treturn fmt.Sprintf(\"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s\",\n\t\t\t\thost, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode)\n\t\tcase r.SslMode == \"verify-ca\" || r.SslMode == \"verify-full\":\n\t\t\tconnection := fmt.Sprintf(\"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s\",\n\t\t\t\thost, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode)\n\t\t\tif len(r.SslRootCert) > 0 && dir.CheckFileExist(r.SslRootCert) {\n\t\t\t\tconnection += fmt.Sprintf(\" sslrootcert=%s\", r.SslRootCert)\n\t\t\t}\n\t\t\tif len(r.SslCert) > 0 && dir.CheckFileExist(r.SslCert) {\n\t\t\t\tconnection += fmt.Sprintf(\" sslcert=%s\", r.SslCert)\n\t\t\t}\n\t\t\tif len(r.SslKey) > 0 && dir.CheckFileExist(r.SslKey) {\n\t\t\t\tconnection += fmt.Sprintf(\" sslkey=%s\", r.SslKey)\n\t\t\t}\n\t\t\treturn connection\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc parsePgSQLHostPort(dbHost string) (host string, port string) {\n\tif strings.Contains(dbHost, \":\") {\n\t\tidx := strings.LastIndex(dbHost, \":\")\n\t\thost, port = dbHost[:idx], dbHost[idx+1:]\n\t} else if len(dbHost) > 0 {\n\t\thost = dbHost\n\t}\n\tif host == \"\" {\n\t\thost = \"127.0.0.1\"\n\t}\n\tif port == \"\" {\n\t\tport = \"5432\"\n\t}\n\treturn host, port\n}\n\n// CheckDatabaseResp check database response\ntype CheckDatabaseResp struct {\n\tConnectionSuccess bool `json:\"connection_success\"`\n}\n\n// InitEnvironmentResp init environment response\ntype InitEnvironmentResp struct {\n\tSuccess            bool   `json:\"success\"`\n\tCreateConfigFailed bool   `json:\"create_config_failed\"`\n\tDefaultConfig      string `json:\"default_config\"`\n\tErrType            string `json:\"err_type\"`\n}\n\n// InitBaseInfoReq init base info request\ntype InitBaseInfoReq struct {\n\tLanguage               string `validate:\"required,gt=0,lte=30\" json:\"lang\"`\n\tSiteName               string `validate:\"required,sanitizer,gt=0,lte=30\" json:\"site_name\"`\n\tSiteURL                string `validate:\"required,gt=0,lte=512,url\" json:\"site_url\"`\n\tContactEmail           string `validate:\"required,email,gt=0,lte=500\" json:\"contact_email\"`\n\tAdminName              string `validate:\"required,gte=2,lte=30\" json:\"name\"`\n\tAdminPassword          string `validate:\"required,gte=8,lte=32\" json:\"password\"`\n\tAdminEmail             string `validate:\"required,email,gt=0,lte=500\" json:\"email\"`\n\tLoginRequired          bool   `json:\"login_required\"`\n\tExternalContentDisplay string `validate:\"required,oneof=always_display ask_before_display\" json:\"external_content_display\"`\n}\n\nfunc (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tif checker.IsInvalidUsername(r.AdminName) {\n\t\terrField := &validator.FormErrorField{\n\t\t\tErrorField: \"name\",\n\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t}\n\t\terrFields = append(errFields, errField)\n\t\treturn errFields, errors.BadRequest(reason.UsernameInvalid)\n\t}\n\treturn\n}\n\nfunc (r *InitBaseInfoReq) FormatSiteUrl() {\n\tparsedUrl, err := url.Parse(r.SiteURL)\n\tif err != nil {\n\t\treturn\n\t}\n\tr.SiteURL = fmt.Sprintf(\"%s://%s\", parsedUrl.Scheme, parsedUrl.Host)\n\tif len(parsedUrl.Path) > 0 {\n\t\tr.SiteURL += parsedUrl.Path\n\t\tr.SiteURL = strings.TrimSuffix(r.SiteURL, \"/\")\n\t}\n}\n"
  },
  {
    "path": "internal/install/install_server.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage install\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\n\t\"github.com/apache/answer/configs\"\n\t\"github.com/apache/answer/internal/base/conf\"\n\t\"github.com/apache/answer/ui\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst UIStaticPath = \"build/static\"\n\ntype _resource struct {\n\tfs embed.FS\n}\n\n// Open to implement the interface by http.FS required\nfunc (r *_resource) Open(name string) (fs.File, error) {\n\tname = fmt.Sprintf(UIStaticPath+\"/%s\", name)\n\tlog.Debugf(\"open static path %s\", name)\n\treturn r.fs.Open(name)\n}\n\n// NewInstallHTTPServer new install http server.\nfunc NewInstallHTTPServer() *gin.Engine {\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.New()\n\n\tc := &conf.AllConfig{}\n\t_ = yaml.Unmarshal(configs.Config, c)\n\n\tr.GET(\"/healthz\", func(ctx *gin.Context) { ctx.String(200, \"OK\") })\n\tr.StaticFS(c.UI.BaseURL+\"/static\", http.FS(&_resource{\n\t\tfs: ui.Build,\n\t}))\n\n\t// read default config file and extract ui config\n\tinstallApi := r.Group(\"\")\n\tinstallApi.GET(c.UI.BaseURL+\"/\", CheckConfigFileAndRedirectToInstallPage)\n\tinstallApi.GET(c.UI.BaseURL+\"/install\", WebPage)\n\tinstallApi.GET(c.UI.BaseURL+\"/50x\", WebPage)\n\tinstallApi.GET(c.UI.APIBaseURL+\"/installation/language/config\", GetLangMapping)\n\tinstallApi.GET(c.UI.APIBaseURL+\"/installation/language/options\", LangOptions)\n\tinstallApi.POST(c.UI.APIBaseURL+\"/installation/db/check\", CheckDatabase)\n\tinstallApi.POST(c.UI.APIBaseURL+\"/installation/config-file/check\", CheckConfigFile)\n\tinstallApi.POST(c.UI.APIBaseURL+\"/installation/init\", InitEnvironment)\n\tinstallApi.POST(c.UI.APIBaseURL+\"/installation/base-info\", InitBaseInfo)\n\n\tr.NoRoute(func(ctx *gin.Context) {\n\t\tctx.Redirect(http.StatusFound, \"/50x\")\n\t})\n\treturn r\n}\n\nfunc WebPage(c *gin.Context) {\n\tfilePath := \"\"\n\tvar file []byte\n\tvar err error\n\tfilePath = \"build/index.html\"\n\tc.Header(\"content-type\", \"text/html;charset=utf-8\")\n\tfile, err = ui.Build.ReadFile(filePath)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\tc.String(http.StatusOK, string(file))\n}\n"
  },
  {
    "path": "internal/migrations/init.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/repo/revision\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"xorm.io/xorm\"\n)\n\ntype Mentor struct {\n\tctx      context.Context\n\tengine   *xorm.Engine\n\tuserData *InitNeedUserInputData\n\terr      error\n\tDone     bool\n}\n\nfunc NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor {\n\treturn &Mentor{ctx: ctx, engine: engine, userData: data}\n}\n\ntype InitNeedUserInputData struct {\n\tLanguage               string\n\tSiteName               string\n\tSiteURL                string\n\tContactEmail           string\n\tAdminName              string\n\tAdminPassword          string\n\tAdminEmail             string\n\tLoginRequired          bool\n\tExternalContentDisplay string\n}\n\nfunc (m *Mentor) InitDB() error {\n\tm.do(\"check table exist\", m.checkTableExist)\n\tm.do(\"sync table\", m.syncTable)\n\tm.do(\"init version table\", m.initVersionTable)\n\tm.do(\"init admin user\", m.initAdminUser)\n\tm.do(\"init config\", m.initConfig)\n\tm.do(\"init default privileges config\", m.initDefaultRankPrivileges)\n\tm.do(\"init role\", m.initRole)\n\tm.do(\"init power\", m.initPower)\n\tm.do(\"init role power rel\", m.initRolePowerRel)\n\tm.do(\"init admin user role rel\", m.initAdminUserRoleRel)\n\tm.do(\"init site info interface\", m.initSiteInfoInterface)\n\tm.do(\"init site info users settings\", m.initSiteInfoUsersSettings)\n\tm.do(\"init site info general config\", m.initSiteInfoGeneralData)\n\tm.do(\"init site info login config\", m.initSiteInfoLoginConfig)\n\tm.do(\"init site info theme config\", m.initSiteInfoThemeConfig)\n\tm.do(\"init site info seo config\", m.initSiteInfoSEOConfig)\n\tm.do(\"init site info user config\", m.initSiteInfoUsersConfig)\n\tm.do(\"init site info privilege rank\", m.initSiteInfoPrivilegeRank)\n\tm.do(\"init site info write\", m.initSiteInfoAdvanced)\n\tm.do(\"init site info write\", m.initSiteInfoQuestions)\n\tm.do(\"init site info write\", m.initSiteInfoTags)\n\tm.do(\"init site info security\", m.initSiteInfoSecurityConfig)\n\tm.do(\"init default content\", m.initDefaultContent)\n\tm.do(\"init default badges\", m.initDefaultBadges)\n\tm.do(\"init default ai config\", m.initSiteInfoAI)\n\tm.do(\"init default MCP config\", m.initSiteInfoMCP)\n\treturn m.err\n}\n\nfunc (m *Mentor) do(taskName string, fn func()) {\n\tif m.err != nil || m.Done {\n\t\treturn\n\t}\n\tfn()\n\tif m.err != nil {\n\t\tm.err = fmt.Errorf(\"%s failed: %s\", taskName, m.err)\n\t}\n}\n\nfunc (m *Mentor) checkTableExist() {\n\tm.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{})\n\tif m.Done {\n\t\tfmt.Println(\"[database] already exists\")\n\t}\n}\n\nfunc (m *Mentor) syncTable() {\n\tm.err = m.engine.Context(m.ctx).Sync(tables...)\n}\n\nfunc (m *Mentor) initVersionTable() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})\n}\n\nfunc (m *Mentor) initAdminUser() {\n\tgenerateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.User{\n\t\tID:           \"1\",\n\t\tUsername:     m.userData.AdminName,\n\t\tPass:         string(generateFromPassword),\n\t\tEMail:        m.userData.AdminEmail,\n\t\tMailStatus:   1,\n\t\tNoticeStatus: 1,\n\t\tStatus:       1,\n\t\tRank:         1,\n\t\tDisplayName:  m.userData.AdminName,\n\t})\n}\n\nfunc (m *Mentor) initConfig() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable)\n}\n\nfunc (m *Mentor) initDefaultRankPrivileges() {\n\tchooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2)\n\tfor _, privilege := range chooseOption.Privileges {\n\t\t_, err := m.engine.Context(m.ctx).Update(\n\t\t\t&entity.Config{Value: fmt.Sprintf(\"%d\", privilege.Value)},\n\t\t\t&entity.Config{Key: privilege.Key},\n\t\t)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n}\n\nfunc (m *Mentor) initRole() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(roles)\n}\n\nfunc (m *Mentor) initPower() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(powers)\n}\n\nfunc (m *Mentor) initRolePowerRel() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels)\n}\n\nfunc (m *Mentor) initAdminUserRoleRel() {\n\t_, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel)\n}\n\nfunc (m *Mentor) initSiteInfoInterface() {\n\tnow := time.Now()\n\tzoneName, offset := now.In(time.Local).Zone()\n\n\tlocalTimezone := \"UTC\"\n\tfor _, tz := range constant.Timezones {\n\t\tloc, err := time.LoadLocation(tz)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\ttzNow := now.In(loc)\n\t\ttzName, tzOffset := tzNow.Zone()\n\n\t\tif tzName == zoneName && tzOffset == offset {\n\t\t\tlocalTimezone = tz\n\t\t\tbreak\n\t\t}\n\t}\n\n\tinterfaceData := map[string]string{\n\t\t\"language\":  m.userData.Language,\n\t\t\"time_zone\": localTimezone,\n\t}\n\tinterfaceDataBytes, _ := json.Marshal(interfaceData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"interface_settings\",\n\t\tContent: string(interfaceDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoUsersSettings() {\n\tusersSettings := map[string]any{\n\t\t\"default_avatar\":    \"gravatar\",\n\t\t\"gravatar_base_url\": \"https://www.gravatar.com/avatar/\",\n\t}\n\tusersSettingsDataBytes, _ := json.Marshal(usersSettings)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"users_settings\",\n\t\tContent: string(usersSettingsDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoGeneralData() {\n\tgeneralData := map[string]string{\n\t\t\"name\":          m.userData.SiteName,\n\t\t\"site_url\":      m.userData.SiteURL,\n\t\t\"contact_email\": m.userData.ContactEmail,\n\t}\n\tgeneralDataBytes, _ := json.Marshal(generalData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"general\",\n\t\tContent: string(generalDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoLoginConfig() {\n\tloginConfig := map[string]any{\n\t\t\"allow_new_registrations\":   true,\n\t\t\"allow_email_registrations\": true,\n\t\t\"allow_password_login\":      true,\n\t}\n\tloginConfigDataBytes, _ := json.Marshal(loginConfig)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"login\",\n\t\tContent: string(loginConfigDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoSecurityConfig() {\n\tsecurityConfig := map[string]any{\n\t\t\"login_required\":           m.userData.LoginRequired,\n\t\t\"external_content_display\": m.userData.ExternalContentDisplay,\n\t\t\"check_update\":             true,\n\t}\n\tsecurityConfigDataBytes, _ := json.Marshal(securityConfig)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"security\",\n\t\tContent: string(securityConfigDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoThemeConfig() {\n\tthemeConfig := fmt.Sprintf(`{\"theme\":\"default\",\"theme_config\":{\"default\":{\"navbar_style\":\"#0033ff\",\"primary_color\":\"#0033ff\"}},\"layout\":\"%s\"}`, constant.ThemeLayoutFullWidth)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"theme\",\n\t\tContent: themeConfig,\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoSEOConfig() {\n\tseoData := map[string]any{\n\t\t\"permalink\": constant.PermalinkQuestionID,\n\t\t\"robots\":    defaultSEORobotTxt + m.userData.SiteURL + \"/sitemap.xml\",\n\t}\n\tseoDataBytes, _ := json.Marshal(seoData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"seo\",\n\t\tContent: string(seoDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoUsersConfig() {\n\tusersData := map[string]any{\n\t\t\"default_avatar\":            \"gravatar\",\n\t\t\"gravatar_base_url\":         \"https://www.gravatar.com/avatar/\",\n\t\t\"allow_update_display_name\": true,\n\t\t\"allow_update_username\":     true,\n\t\t\"allow_update_avatar\":       true,\n\t\t\"allow_update_bio\":          true,\n\t\t\"allow_update_website\":      true,\n\t\t\"allow_update_location\":     true,\n\t}\n\tusersDataBytes, _ := json.Marshal(usersData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"users\",\n\t\tContent: string(usersDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoPrivilegeRank() {\n\tprivilegeRankData := map[string]any{\n\t\t\"level\": schema.PrivilegeLevel2,\n\t}\n\tprivilegeRankDataBytes, _ := json.Marshal(privilegeRankData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"privileges\",\n\t\tContent: string(privilegeRankDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoAdvanced() {\n\tadvancedData := map[string]any{\n\t\t\"max_image_size\":                   4,\n\t\t\"max_attachment_size\":              8,\n\t\t\"max_image_megapixel\":              40,\n\t\t\"authorized_image_extensions\":      []string{\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\"},\n\t\t\"authorized_attachment_extensions\": []string{},\n\t}\n\tadvancedDataBytes, _ := json.Marshal(advancedData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"advanced\",\n\t\tContent: string(advancedDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoQuestions() {\n\tquestionsData := map[string]any{\n\t\t\"min_tags\":        1,\n\t\t\"min_content\":     6,\n\t\t\"restrict_answer\": true,\n\t}\n\tquestionsDataBytes, _ := json.Marshal(questionsData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"questions\",\n\t\tContent: string(questionsDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initSiteInfoTags() {\n\ttagsData := map[string]any{\n\t\t\"required_tag\":   false,\n\t\t\"recommend_tags\": []string{},\n\t\t\"reserved_tags\":  []string{},\n\t}\n\ttagsDataBytes, _ := json.Marshal(tagsData)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    \"tags\",\n\t\tContent: string(tagsDataBytes),\n\t\tStatus:  1,\n\t})\n}\n\nfunc (m *Mentor) initDefaultContent() {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine})\n\trevisionRepo := revision.NewRevisionRepo(&data.Data{DB: m.engine}, uniqueIDRepo)\n\tnow := time.Now()\n\n\ttagId, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Tag{}.TableName())\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\n\tq1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\n\ta1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\n\tq2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName())\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\n\ta2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName())\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\n\ttag := &entity.Tag{\n\t\tID:            tagId,\n\t\tSlugName:      \"support\",\n\t\tDisplayName:   \"support\",\n\t\tOriginalText:  \"For general support questions.\",\n\t\tParsedText:    \"<p>For general support questions.</p>\",\n\t\tUserID:        \"1\",\n\t\tQuestionCount: 2,\n\t\tStatus:        entity.TagStatusAvailable,\n\t\tRevisionID:    \"0\",\n\t}\n\n\tq1 := &entity.Question{\n\t\tID:               q1Id,\n\t\tCreatedAt:        now,\n\t\tUserID:           \"1\",\n\t\tLastEditUserID:   \"1\",\n\t\tTitle:            \"What is a tag?\",\n\t\tOriginalText:     \"When asking a question, we need to choose tags. What are tags and why should I use them?\",\n\t\tParsedText:       \"<p>When asking a question, we need to choose tags. What are tags and why should I use them?</p>\",\n\t\tPin:              entity.QuestionUnPin,\n\t\tShow:             entity.QuestionShow,\n\t\tStatus:           entity.QuestionStatusAvailable,\n\t\tAnswerCount:      1,\n\t\tAcceptedAnswerID: \"0\",\n\t\tLastAnswerID:     a1Id,\n\t\tPostUpdateTime:   now,\n\t\tRevisionID:       \"0\",\n\t}\n\n\ta1 := &entity.Answer{\n\t\tID:             a1Id,\n\t\tCreatedAt:      now,\n\t\tQuestionID:     q1Id,\n\t\tUserID:         \"1\",\n\t\tLastEditUserID: \"0\",\n\t\tOriginalText:   \"Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.\",\n\t\tParsedText:     \"<p>Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.</p>\",\n\t\tStatus:         entity.AnswerStatusAvailable,\n\t\tRevisionID:     \"0\",\n\t}\n\n\tq2 := &entity.Question{\n\t\tID:               q2Id,\n\t\tCreatedAt:        now,\n\t\tUserID:           \"1\",\n\t\tLastEditUserID:   \"1\",\n\t\tTitle:            \"What is reputation and how do I earn them?\",\n\t\tOriginalText:     \"I see that each user has reputation points, What is it and how do I earn them?\",\n\t\tParsedText:       \"<p>I see that each user has reputation points, What is it and how do I earn them?</p>\",\n\t\tPin:              entity.QuestionUnPin,\n\t\tShow:             entity.QuestionShow,\n\t\tStatus:           entity.QuestionStatusAvailable,\n\t\tAnswerCount:      1,\n\t\tAcceptedAnswerID: \"0\",\n\t\tLastAnswerID:     a2Id,\n\t\tPostUpdateTime:   now,\n\t\tRevisionID:       \"0\",\n\t}\n\n\ta2 := &entity.Answer{\n\t\tID:             a2Id,\n\t\tCreatedAt:      now,\n\t\tQuestionID:     q2Id,\n\t\tUserID:         \"1\",\n\t\tLastEditUserID: \"0\",\n\t\tOriginalText:   \"Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.\",\n\t\tParsedText:     \"<p>Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.</p>\",\n\t\tStatus:         entity.AnswerStatusAvailable,\n\t\tRevisionID:     \"0\",\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(tag)\n\tif m.err != nil {\n\t\treturn\n\t}\n\ttagContent, err := json.Marshal(tag)\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\tm.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{\n\t\tUserID:   tag.UserID,\n\t\tObjectID: tag.ID,\n\t\tTitle:    tag.SlugName,\n\t\tContent:  string(tagContent),\n\t\tStatus:   entity.RevisionReviewPassStatus,\n\t}, true)\n\tif m.err != nil {\n\t\treturn\n\t}\n\ttagForRevision := &entity.TagSimpleInfoForRevision{\n\t\tID:              tag.ID,\n\t\tMainTagID:       tag.MainTagID,\n\t\tMainTagSlugName: tag.MainTagSlugName,\n\t\tSlugName:        tag.SlugName,\n\t\tDisplayName:     tag.DisplayName,\n\t\tRecommend:       tag.Recommend,\n\t\tReserved:        tag.Reserved,\n\t\tRevisionID:      tag.RevisionID,\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(q1)\n\tif m.err != nil {\n\t\treturn\n\t}\n\tq1Revision := &entity.QuestionWithTagsRevision{\n\t\tQuestion: *q1,\n\t\tTags:     []*entity.TagSimpleInfoForRevision{tagForRevision},\n\t}\n\tq1Content, err := json.Marshal(q1Revision)\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\tm.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{\n\t\tUserID:   q1.UserID,\n\t\tObjectID: q1.ID,\n\t\tTitle:    q1.Title,\n\t\tContent:  string(q1Content),\n\t\tStatus:   entity.RevisionReviewPassStatus,\n\t}, true)\n\tif m.err != nil {\n\t\treturn\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(a1)\n\tif m.err != nil {\n\t\treturn\n\t}\n\ta1Content, err := json.Marshal(a1)\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\tm.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{\n\t\tUserID:   a1.UserID,\n\t\tObjectID: a1.ID,\n\t\tContent:  string(a1Content),\n\t\tStatus:   entity.RevisionReviewPassStatus,\n\t}, true)\n\tif m.err != nil {\n\t\treturn\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{\n\t\tObjectID: q1.ID,\n\t\tTagID:    tag.ID,\n\t\tStatus:   entity.TagRelStatusAvailable,\n\t})\n\tif m.err != nil {\n\t\treturn\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(q2)\n\tif m.err != nil {\n\t\treturn\n\t}\n\tq2Revision := &entity.QuestionWithTagsRevision{\n\t\tQuestion: *q2,\n\t\tTags:     []*entity.TagSimpleInfoForRevision{tagForRevision},\n\t}\n\tq2Content, err := json.Marshal(q2Revision)\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\tm.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{\n\t\tUserID:   q2.UserID,\n\t\tObjectID: q2.ID,\n\t\tTitle:    q2.Title,\n\t\tContent:  string(q2Content),\n\t\tStatus:   entity.RevisionReviewPassStatus,\n\t}, true)\n\tif m.err != nil {\n\t\treturn\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(a2)\n\tif m.err != nil {\n\t\treturn\n\t}\n\ta2Content, err := json.Marshal(a2)\n\tif err != nil {\n\t\tm.err = err\n\t\treturn\n\t}\n\tm.err = revisionRepo.AddRevision(m.ctx, &entity.Revision{\n\t\tUserID:   a2.UserID,\n\t\tObjectID: a2.ID,\n\t\tContent:  string(a2Content),\n\t\tStatus:   entity.RevisionReviewPassStatus,\n\t}, true)\n\tif m.err != nil {\n\t\treturn\n\t}\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{\n\t\tObjectID: q2.ID,\n\t\tTagID:    tag.ID,\n\t\tStatus:   entity.TagRelStatusAvailable,\n\t})\n\tif m.err != nil {\n\t\treturn\n\t}\n}\n\nfunc (m *Mentor) initDefaultBadges() {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine})\n\n\t_, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable)\n\tif m.err != nil {\n\t\treturn\n\t}\n\tfor _, badge := range defaultBadgeTable {\n\t\tbadge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName())\n\t\tif m.err != nil {\n\t\t\treturn\n\t\t}\n\t\tif _, m.err = m.engine.Context(m.ctx).Insert(badge); m.err != nil {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (m *Mentor) initSiteInfoAI() {\n\tcontent := &schema.SiteAIReq{\n\t\tPromptConfig: &schema.AIPromptConfig{\n\t\t\tZhCN: constant.DefaultAIPromptConfigZhCN,\n\t\t\tEnUS: constant.DefaultAIPromptConfigEnUS,\n\t\t},\n\t}\n\twriteDataBytes, _ := json.Marshal(content)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    constant.SiteTypeAI,\n\t\tContent: string(writeDataBytes),\n\t\tStatus:  1,\n\t})\n}\nfunc (m *Mentor) initSiteInfoMCP() {\n\tcontent := &schema.SiteMCPReq{\n\t\tEnabled: true,\n\t}\n\twriteDataBytes, _ := json.Marshal(content)\n\t_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{\n\t\tType:    constant.SiteTypeMCP,\n\t\tContent: string(writeDataBytes),\n\t\tStatus:  1,\n\t})\n}\n"
  },
  {
    "path": "internal/migrations/init_data.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/permission\"\n)\n\nconst (\n\tdefaultSEORobotTxt = `User-agent: *\nDisallow: /admin\nDisallow: /search\nDisallow: /install\nDisallow: /review\nDisallow: /users/login\nDisallow: /users/register\nDisallow: /users/account-recovery\nDisallow: /users/oauth/*\nDisallow: /users/*/*\nDisallow: /answer/api\nDisallow: /*?code*\nDisallow: /swagger/*\n\nSitemap: `\n)\n\nvar (\n\ttables = []any{\n\t\t&entity.Activity{},\n\t\t&entity.Answer{},\n\t\t&entity.Collection{},\n\t\t&entity.CollectionGroup{},\n\t\t&entity.Comment{},\n\t\t&entity.Config{},\n\t\t&entity.Meta{},\n\t\t&entity.Notification{},\n\t\t&entity.Question{},\n\t\t&entity.QuestionLink{},\n\t\t&entity.Report{},\n\t\t&entity.Revision{},\n\t\t&entity.SiteInfo{},\n\t\t&entity.Tag{},\n\t\t&entity.TagRel{},\n\t\t&entity.Uniqid{},\n\t\t&entity.User{},\n\t\t&entity.Version{},\n\t\t&entity.Role{},\n\t\t&entity.RolePowerRel{},\n\t\t&entity.Power{},\n\t\t&entity.UserRoleRel{},\n\t\t&entity.PluginConfig{},\n\t\t&entity.UserExternalLogin{},\n\t\t&entity.UserNotificationConfig{},\n\t\t&entity.PluginUserConfig{},\n\t\t&entity.Review{},\n\t\t&entity.Badge{},\n\t\t&entity.BadgeGroup{},\n\t\t&entity.BadgeAward{},\n\t\t&entity.FileRecord{},\n\t\t&entity.PluginKVStorage{},\n\t\t&entity.APIKey{},\n\t\t&entity.AIConversation{},\n\t\t&entity.AIConversationRecord{},\n\t}\n\n\troles = []*entity.Role{\n\t\t{ID: 1, Name: \"User\", Description: \"Default with no special access.\"},\n\t\t{ID: 2, Name: \"Admin\", Description: \"Have the full power to access the site.\"},\n\t\t{ID: 3, Name: \"Moderator\", Description: \"Has access to all posts except admin settings.\"},\n\t}\n\n\tpowers = []*entity.Power{\n\t\t{ID: 1, Name: \"admin access\", PowerType: permission.AdminAccess, Description: \"admin access\"},\n\t\t{ID: 2, Name: \"question add\", PowerType: permission.QuestionAdd, Description: \"question add\"},\n\t\t{ID: 3, Name: \"question edit\", PowerType: permission.QuestionEdit, Description: \"question edit\"},\n\t\t{ID: 4, Name: \"question edit without review\", PowerType: permission.QuestionEditWithoutReview, Description: \"question edit without review\"},\n\t\t{ID: 5, Name: \"question delete\", PowerType: permission.QuestionDelete, Description: \"question delete\"},\n\t\t{ID: 6, Name: \"question close\", PowerType: permission.QuestionClose, Description: \"question close\"},\n\t\t{ID: 7, Name: \"question reopen\", PowerType: permission.QuestionReopen, Description: \"question reopen\"},\n\t\t{ID: 8, Name: \"question vote up\", PowerType: permission.QuestionVoteUp, Description: \"question vote up\"},\n\t\t{ID: 9, Name: \"question vote down\", PowerType: permission.QuestionVoteDown, Description: \"question vote down\"},\n\t\t{ID: 10, Name: \"answer add\", PowerType: permission.AnswerAdd, Description: \"answer add\"},\n\t\t{ID: 11, Name: \"answer edit\", PowerType: permission.AnswerEdit, Description: \"answer edit\"},\n\t\t{ID: 12, Name: \"answer edit without review\", PowerType: permission.AnswerEditWithoutReview, Description: \"answer edit without review\"},\n\t\t{ID: 13, Name: \"answer delete\", PowerType: permission.AnswerDelete, Description: \"answer delete\"},\n\t\t{ID: 14, Name: \"answer accept\", PowerType: permission.AnswerAccept, Description: \"answer accept\"},\n\t\t{ID: 15, Name: \"answer vote up\", PowerType: permission.AnswerVoteUp, Description: \"answer vote up\"},\n\t\t{ID: 16, Name: \"answer vote down\", PowerType: permission.AnswerVoteDown, Description: \"answer vote down\"},\n\t\t{ID: 17, Name: \"comment add\", PowerType: permission.CommentAdd, Description: \"comment add\"},\n\t\t{ID: 18, Name: \"comment edit\", PowerType: permission.CommentEdit, Description: \"comment edit\"},\n\t\t{ID: 19, Name: \"comment delete\", PowerType: permission.CommentDelete, Description: \"comment delete\"},\n\t\t{ID: 20, Name: \"comment vote up\", PowerType: permission.CommentVoteUp, Description: \"comment vote up\"},\n\t\t{ID: 21, Name: \"comment vote down\", PowerType: permission.CommentVoteDown, Description: \"comment vote down\"},\n\t\t{ID: 22, Name: \"report add\", PowerType: permission.ReportAdd, Description: \"report add\"},\n\t\t{ID: 23, Name: \"tag add\", PowerType: permission.TagAdd, Description: \"tag add\"},\n\t\t{ID: 24, Name: \"tag edit\", PowerType: permission.TagEdit, Description: \"tag edit\"},\n\t\t{ID: 25, Name: \"tag edit without review\", PowerType: permission.TagEditWithoutReview, Description: \"tag edit without review\"},\n\t\t{ID: 26, Name: \"tag edit slug name\", PowerType: permission.TagEditSlugName, Description: \"tag edit slug name\"},\n\t\t{ID: 27, Name: \"tag delete\", PowerType: permission.TagDelete, Description: \"tag delete\"},\n\t\t{ID: 28, Name: \"tag synonym\", PowerType: permission.TagSynonym, Description: \"tag synonym\"},\n\t\t{ID: 29, Name: \"link url limit\", PowerType: permission.LinkUrlLimit, Description: \"link url limit\"},\n\t\t{ID: 30, Name: \"vote detail\", PowerType: permission.VoteDetail, Description: \"vote detail\"},\n\t\t{ID: 31, Name: \"answer audit\", PowerType: permission.AnswerAudit, Description: \"answer audit\"},\n\t\t{ID: 32, Name: \"question audit\", PowerType: permission.QuestionAudit, Description: \"question audit\"},\n\t\t{ID: 33, Name: \"tag audit\", PowerType: permission.TagAudit, Description: \"tag audit\"},\n\t\t{ID: 34, Name: \"question pin\", PowerType: permission.QuestionPin, Description: \"top the question\"},\n\t\t{ID: 35, Name: \"question hide\", PowerType: permission.QuestionHide, Description: \"hide  the question\"},\n\t\t{ID: 36, Name: \"question unpin\", PowerType: permission.QuestionUnPin, Description: \"untop the question\"},\n\t\t{ID: 37, Name: \"question show\", PowerType: permission.QuestionShow, Description: \"show the question\"},\n\t\t{ID: 38, Name: \"invite someone to answer\", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: \"invite someone to answer\"},\n\t\t{ID: 39, Name: \"recover answer\", PowerType: permission.AnswerUnDelete, Description: \"recover deleted answer\"},\n\t\t{ID: 40, Name: \"recover question\", PowerType: permission.QuestionUnDelete, Description: \"recover deleted question\"},\n\t\t{ID: 41, Name: \"recover tag\", PowerType: permission.TagUnDelete, Description: \"recover deleted tag\"},\n\t}\n\n\trolePowerRels = []*entity.RolePowerRel{\n\t\t{RoleID: 2, PowerType: permission.AdminAccess},\n\t\t{RoleID: 2, PowerType: permission.QuestionAdd},\n\t\t{RoleID: 2, PowerType: permission.QuestionEdit},\n\t\t{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.QuestionDelete},\n\t\t{RoleID: 2, PowerType: permission.QuestionClose},\n\t\t{RoleID: 2, PowerType: permission.QuestionReopen},\n\t\t{RoleID: 2, PowerType: permission.QuestionVoteUp},\n\t\t{RoleID: 2, PowerType: permission.QuestionVoteDown},\n\t\t{RoleID: 2, PowerType: permission.AnswerAdd},\n\t\t{RoleID: 2, PowerType: permission.AnswerEdit},\n\t\t{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.AnswerDelete},\n\t\t{RoleID: 2, PowerType: permission.AnswerAccept},\n\t\t{RoleID: 2, PowerType: permission.AnswerVoteUp},\n\t\t{RoleID: 2, PowerType: permission.AnswerVoteDown},\n\t\t{RoleID: 2, PowerType: permission.CommentAdd},\n\t\t{RoleID: 2, PowerType: permission.CommentEdit},\n\t\t{RoleID: 2, PowerType: permission.CommentDelete},\n\t\t{RoleID: 2, PowerType: permission.CommentVoteUp},\n\t\t{RoleID: 2, PowerType: permission.CommentVoteDown},\n\t\t{RoleID: 2, PowerType: permission.ReportAdd},\n\t\t{RoleID: 2, PowerType: permission.TagAdd},\n\t\t{RoleID: 2, PowerType: permission.TagEdit},\n\t\t{RoleID: 2, PowerType: permission.TagEditSlugName},\n\t\t{RoleID: 2, PowerType: permission.TagEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.TagDelete},\n\t\t{RoleID: 2, PowerType: permission.TagSynonym},\n\t\t{RoleID: 2, PowerType: permission.LinkUrlLimit},\n\t\t{RoleID: 2, PowerType: permission.VoteDetail},\n\t\t{RoleID: 2, PowerType: permission.AnswerAudit},\n\t\t{RoleID: 2, PowerType: permission.QuestionAudit},\n\t\t{RoleID: 2, PowerType: permission.TagAudit},\n\t\t{RoleID: 2, PowerType: permission.TagUseReservedTag},\n\t\t{RoleID: 2, PowerType: permission.QuestionPin},\n\t\t{RoleID: 2, PowerType: permission.QuestionHide},\n\t\t{RoleID: 2, PowerType: permission.QuestionUnPin},\n\t\t{RoleID: 2, PowerType: permission.QuestionShow},\n\t\t{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},\n\t\t{RoleID: 2, PowerType: permission.AnswerUnDelete},\n\t\t{RoleID: 2, PowerType: permission.QuestionUnDelete},\n\t\t{RoleID: 2, PowerType: permission.TagUnDelete},\n\n\t\t{RoleID: 3, PowerType: permission.QuestionAdd},\n\t\t{RoleID: 3, PowerType: permission.QuestionEdit},\n\t\t{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.QuestionDelete},\n\t\t{RoleID: 3, PowerType: permission.QuestionClose},\n\t\t{RoleID: 3, PowerType: permission.QuestionReopen},\n\t\t{RoleID: 3, PowerType: permission.QuestionVoteUp},\n\t\t{RoleID: 3, PowerType: permission.QuestionVoteDown},\n\t\t{RoleID: 3, PowerType: permission.AnswerAdd},\n\t\t{RoleID: 3, PowerType: permission.AnswerEdit},\n\t\t{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.AnswerDelete},\n\t\t{RoleID: 3, PowerType: permission.AnswerAccept},\n\t\t{RoleID: 3, PowerType: permission.AnswerVoteUp},\n\t\t{RoleID: 3, PowerType: permission.AnswerVoteDown},\n\t\t{RoleID: 3, PowerType: permission.CommentAdd},\n\t\t{RoleID: 3, PowerType: permission.CommentEdit},\n\t\t{RoleID: 3, PowerType: permission.CommentDelete},\n\t\t{RoleID: 3, PowerType: permission.CommentVoteUp},\n\t\t{RoleID: 3, PowerType: permission.CommentVoteDown},\n\t\t{RoleID: 3, PowerType: permission.ReportAdd},\n\t\t{RoleID: 3, PowerType: permission.TagAdd},\n\t\t{RoleID: 3, PowerType: permission.TagEdit},\n\t\t{RoleID: 3, PowerType: permission.TagEditSlugName},\n\t\t{RoleID: 3, PowerType: permission.TagEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.TagDelete},\n\t\t{RoleID: 3, PowerType: permission.TagSynonym},\n\t\t{RoleID: 3, PowerType: permission.LinkUrlLimit},\n\t\t{RoleID: 3, PowerType: permission.VoteDetail},\n\t\t{RoleID: 3, PowerType: permission.AnswerAudit},\n\t\t{RoleID: 3, PowerType: permission.QuestionAudit},\n\t\t{RoleID: 3, PowerType: permission.TagAudit},\n\t\t{RoleID: 3, PowerType: permission.TagUseReservedTag},\n\t\t{RoleID: 3, PowerType: permission.QuestionPin},\n\t\t{RoleID: 3, PowerType: permission.QuestionHide},\n\t\t{RoleID: 3, PowerType: permission.QuestionUnPin},\n\t\t{RoleID: 3, PowerType: permission.QuestionShow},\n\t\t{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},\n\t\t{RoleID: 3, PowerType: permission.AnswerUnDelete},\n\t\t{RoleID: 3, PowerType: permission.QuestionUnDelete},\n\t\t{RoleID: 3, PowerType: permission.TagUnDelete},\n\t}\n\n\tadminUserRoleRel = &entity.UserRoleRel{\n\t\tUserID: \"1\",\n\t\tRoleID: 2,\n\t}\n\n\tdefaultConfigTable = []*entity.Config{\n\t\t{ID: 1, Key: \"answer.accepted\", Value: `15`},\n\t\t{ID: 2, Key: \"answer.voted_up\", Value: `10`},\n\t\t{ID: 3, Key: \"question.voted_up\", Value: `10`},\n\t\t{ID: 4, Key: \"tag.edit_accepted\", Value: `2`},\n\t\t{ID: 5, Key: \"answer.accept\", Value: `2`},\n\t\t{ID: 6, Key: \"answer.voted_down_cancel\", Value: `2`},\n\t\t{ID: 7, Key: \"question.voted_down_cancel\", Value: `2`},\n\t\t{ID: 8, Key: \"answer.vote_down_cancel\", Value: `1`},\n\t\t{ID: 9, Key: \"question.vote_down_cancel\", Value: `1`},\n\t\t{ID: 10, Key: \"user.activated\", Value: `1`},\n\t\t{ID: 11, Key: \"edit.accepted\", Value: `2`},\n\t\t{ID: 12, Key: \"answer.vote_down\", Value: `-1`},\n\t\t{ID: 13, Key: \"question.voted_down\", Value: `-2`},\n\t\t{ID: 14, Key: \"answer.voted_down\", Value: `-2`},\n\t\t{ID: 15, Key: \"answer.accept_cancel\", Value: `-2`},\n\t\t{ID: 16, Key: \"answer.deleted\", Value: `-5`},\n\t\t{ID: 17, Key: \"question.voted_up_cancel\", Value: `-10`},\n\t\t{ID: 18, Key: \"answer.voted_up_cancel\", Value: `-10`},\n\t\t{ID: 19, Key: \"answer.accepted_cancel\", Value: `-15`},\n\t\t{ID: 20, Key: \"object.reported\", Value: `-100`},\n\t\t{ID: 21, Key: \"edit.rejected\", Value: `-2`},\n\t\t{ID: 22, Key: \"daily_rank_limit\", Value: `200`},\n\t\t{ID: 23, Key: \"daily_rank_limit.exclude\", Value: `[\"answer.accepted\"]`},\n\t\t{ID: 24, Key: \"user.follow\", Value: `0`},\n\t\t{ID: 25, Key: \"comment.vote_up\", Value: `0`},\n\t\t{ID: 26, Key: \"comment.vote_up_cancel\", Value: `0`},\n\t\t{ID: 27, Key: \"question.vote_down\", Value: `0`},\n\t\t{ID: 28, Key: \"question.vote_up\", Value: `0`},\n\t\t{ID: 29, Key: \"question.vote_up_cancel\", Value: `0`},\n\t\t{ID: 30, Key: \"answer.vote_up\", Value: `0`},\n\t\t{ID: 31, Key: \"answer.vote_up_cancel\", Value: `0`},\n\t\t{ID: 32, Key: \"question.follow\", Value: `0`},\n\t\t{ID: 33, Key: \"email.config\", Value: `{\"from_name\":\"\",\"from_email\":\"\",\"smtp_host\":\"\",\"smtp_port\":465,\"smtp_password\":\"\",\"smtp_username\":\"\",\"smtp_authentication\":true,\"encryption\":\"\",\"register_title\":\"[{{.SiteName}}] Confirm your new account\",\"register_body\":\"Welcome to {{.SiteName}}<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n\",\"pass_reset_title\":\"[{{.SiteName }}] Password reset\",\"pass_reset_body\":\"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n\",\"change_title\":\"[{{.SiteName}}] Confirm your new email address\",\"change_body\":\"Confirm your new email address for {{.SiteName}}  by clicking on the following link:<br><br>\\n\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.\\n\",\"test_title\":\"[{{.SiteName}}] Test Email\",\"test_body\":\"This is a test email.\",\"new_answer_title\":\"[{{.SiteName}}] {{.DisplayName}} answered your question\",\"new_answer_body\":\"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\",\"new_comment_title\":\"[{{.SiteName}}] {{.DisplayName}} commented on your post\",\"new_comment_body\":\"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"}`},\n\t\t{ID: 35, Key: \"tag.follow\", Value: `0`},\n\t\t{ID: 36, Key: \"rank.question.add\", Value: `1`},\n\t\t{ID: 37, Key: \"rank.question.edit\", Value: `200`},\n\t\t{ID: 38, Key: \"rank.question.delete\", Value: `-1`},\n\t\t{ID: 39, Key: \"rank.question.vote_up\", Value: `15`},\n\t\t{ID: 40, Key: \"rank.question.vote_down\", Value: `125`},\n\t\t{ID: 41, Key: \"rank.answer.add\", Value: `1`},\n\t\t{ID: 42, Key: \"rank.answer.edit\", Value: `200`},\n\t\t{ID: 43, Key: \"rank.answer.delete\", Value: `-1`},\n\t\t{ID: 44, Key: \"rank.answer.accept\", Value: `-1`},\n\t\t{ID: 45, Key: \"rank.answer.vote_up\", Value: `15`},\n\t\t{ID: 46, Key: \"rank.answer.vote_down\", Value: `125`},\n\t\t{ID: 47, Key: \"rank.comment.add\", Value: `1`},\n\t\t{ID: 48, Key: \"rank.comment.edit\", Value: `-1`},\n\t\t{ID: 49, Key: \"rank.comment.delete\", Value: `-1`},\n\t\t{ID: 50, Key: \"rank.report.add\", Value: `1`},\n\t\t{ID: 51, Key: \"rank.tag.add\", Value: `1500`},\n\t\t{ID: 52, Key: \"rank.tag.edit\", Value: `100`},\n\t\t{ID: 53, Key: \"rank.tag.delete\", Value: `-1`},\n\t\t{ID: 54, Key: \"rank.tag.synonym\", Value: `20000`},\n\t\t{ID: 55, Key: \"rank.link.url_limit\", Value: `10`},\n\t\t{ID: 56, Key: \"rank.vote.detail\", Value: `0`},\n\t\t{ID: 57, Key: \"reason.spam\", Value: `{\"name\":\"spam\",\"description\":\"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.\"}`},\n\t\t{ID: 58, Key: \"reason.rude_or_abusive\", Value: `{\"name\":\"rude or abusive\",\"description\":\"A reasonable person would find this content inappropriate for respectful discourse.\"}`},\n\t\t{ID: 59, Key: \"reason.something\", Value: `{\"name\":\"something else\",\"description\":\"This post requires staff attention for another reason not listed above.\",\"content_type\":\"textarea\"}`},\n\t\t{ID: 60, Key: \"reason.a_duplicate\", Value: `{\"name\":\"a duplicate\",\"description\":\"This question has been asked before and already has an answer.\",\"content_type\":\"text\"}`},\n\t\t{ID: 61, Key: \"reason.not_a_answer\", Value: `{\"name\":\"not a answer\",\"description\":\"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.\",\"content_type\":\"\"}`},\n\t\t{ID: 62, Key: \"reason.no_longer_needed\", Value: `{\"name\":\"no longer needed\",\"description\":\"This comment is outdated, conversational or not relevant to this post.\"}`},\n\t\t{ID: 63, Key: \"reason.community_specific\", Value: `{\"name\":\"a community-specific reason\",\"description\":\"This question doesn't meet a community guideline.\"}`},\n\t\t{ID: 64, Key: \"reason.not_clarity\", Value: `{\"name\":\"needs details or clarity\",\"description\":\"This question currently includes multiple questions in one. It should focus on one problem only.\"}`},\n\t\t{ID: 65, Key: \"reason.normal\", Value: `{\"name\":\"normal\",\"description\":\"A normal post available to everyone.\"}`},\n\t\t{ID: 66, Key: \"reason.normal.user\", Value: `{\"name\":\"normal\",\"description\":\"A normal user can ask and answer questions.\"}`},\n\t\t{ID: 67, Key: \"reason.closed\", Value: `{\"name\":\"closed\",\"description\":\"A closed question can't answer, but still can edit, vote and comment.\"}`},\n\t\t{ID: 68, Key: \"reason.deleted\", Value: `{\"name\":\"deleted\",\"description\":\"All reputation gained and lost will be restored.\"}`},\n\t\t{ID: 69, Key: \"reason.deleted.user\", Value: `{\"name\":\"deleted\",\"description\":\"Delete profile, authentication associations.\"}`},\n\t\t{ID: 70, Key: \"reason.suspended\", Value: `{\"name\":\"suspended\",\"description\":\"A suspended user can't log in.\"}`},\n\t\t{ID: 71, Key: \"reason.inactive\", Value: `{\"name\":\"inactive\",\"description\":\"An inactive user must re-validate their email.\"}`},\n\t\t{ID: 72, Key: \"reason.looks_ok\", Value: `{\"name\":\"looks ok\",\"description\":\"This post is good as-is and not low quality.\"}`},\n\t\t{ID: 73, Key: \"reason.needs_edit\", Value: `{\"name\":\"needs edit, and I did it\",\"description\":\"Improve and correct problems with this post yourself.\"}`},\n\t\t{ID: 74, Key: \"reason.needs_close\", Value: `{\"name\":\"needs close\",\"description\":\"A closed question can't answer, but still can edit, vote and comment.\"}`},\n\t\t{ID: 75, Key: \"reason.needs_delete\", Value: `{\"name\":\"needs delete\",\"description\":\"All reputation gained and lost will be restored.\"}`},\n\t\t{ID: 76, Key: \"question.flag.reasons\", Value: `[\"reason.spam\",\"reason.rude_or_abusive\",\"reason.something\",\"reason.a_duplicate\"]`},\n\t\t{ID: 77, Key: \"answer.flag.reasons\", Value: `[\"reason.spam\",\"reason.rude_or_abusive\",\"reason.something\",\"reason.not_a_answer\"]`},\n\t\t{ID: 78, Key: \"comment.flag.reasons\", Value: `[\"reason.spam\",\"reason.rude_or_abusive\",\"reason.something\",\"reason.no_longer_needed\"]`},\n\t\t{ID: 79, Key: \"question.close.reasons\", Value: `[\"reason.a_duplicate\",\"reason.community_specific\",\"reason.not_clarity\",\"reason.something\"]`},\n\t\t{ID: 80, Key: \"question.status.reasons\", Value: `[\"reason.normal\",\"reason.closed\",\"reason.deleted\"]`},\n\t\t{ID: 81, Key: \"answer.status.reasons\", Value: `[\"reason.normal\",\"reason.deleted\"]`},\n\t\t{ID: 82, Key: \"comment.status.reasons\", Value: `[\"reason.normal\",\"reason.deleted\"]`},\n\t\t{ID: 83, Key: \"user.status.reasons\", Value: `[\"reason.normal.user\",\"reason.suspended\",\"reason.deleted.user\",\"reason.inactive\"]`},\n\t\t{ID: 84, Key: \"question.review.reasons\", Value: `[\"reason.looks_ok\",\"reason.needs_edit\",\"reason.needs_close\",\"reason.needs_delete\"]`},\n\t\t{ID: 85, Key: \"answer.review.reasons\", Value: `[\"reason.looks_ok\",\"reason.needs_edit\",\"reason.needs_delete\"]`},\n\t\t{ID: 86, Key: \"comment.review.reasons\", Value: `[\"reason.looks_ok\",\"reason.needs_edit\",\"reason.needs_delete\"]`},\n\t\t{ID: 87, Key: \"question.asked\", Value: `0`},\n\t\t{ID: 88, Key: \"question.closed\", Value: `0`},\n\t\t{ID: 89, Key: \"question.reopened\", Value: `0`},\n\t\t{ID: 90, Key: \"question.answered\", Value: `0`},\n\t\t{ID: 91, Key: \"question.commented\", Value: `0`},\n\t\t{ID: 92, Key: \"question.accept\", Value: `0`},\n\t\t{ID: 93, Key: \"question.edited\", Value: `0`},\n\t\t{ID: 94, Key: \"question.rollback\", Value: `0`},\n\t\t{ID: 95, Key: \"question.deleted\", Value: `0`},\n\t\t{ID: 96, Key: \"question.undeleted\", Value: `0`},\n\t\t{ID: 97, Key: \"answer.answered\", Value: `0`},\n\t\t{ID: 98, Key: \"answer.commented\", Value: `0`},\n\t\t{ID: 99, Key: \"answer.edited\", Value: `0`},\n\t\t{ID: 100, Key: \"answer.rollback\", Value: `0`},\n\t\t{ID: 101, Key: \"answer.undeleted\", Value: `0`},\n\t\t{ID: 102, Key: \"tag.created\", Value: `0`},\n\t\t{ID: 103, Key: \"tag.edited\", Value: `0`},\n\t\t{ID: 104, Key: \"tag.rollback\", Value: `0`},\n\t\t{ID: 105, Key: \"tag.deleted\", Value: `0`},\n\t\t{ID: 106, Key: \"tag.undeleted\", Value: `0`},\n\t\t{ID: 107, Key: \"rank.comment.vote_up\", Value: `1`},\n\t\t{ID: 108, Key: \"rank.comment.vote_down\", Value: `1`},\n\t\t{ID: 109, Key: \"rank.question.edit_without_review\", Value: `2000`},\n\t\t{ID: 110, Key: \"rank.answer.edit_without_review\", Value: `2000`},\n\t\t{ID: 111, Key: \"rank.tag.edit_without_review\", Value: `20000`},\n\t\t{ID: 112, Key: \"rank.answer.audit\", Value: `2000`},\n\t\t{ID: 113, Key: \"rank.question.audit\", Value: `2000`},\n\t\t{ID: 114, Key: \"rank.tag.audit\", Value: `20000`},\n\t\t{ID: 115, Key: \"rank.question.close\", Value: `-1`},\n\t\t{ID: 116, Key: \"rank.question.reopen\", Value: `-1`},\n\t\t{ID: 117, Key: \"rank.tag.use_reserved_tag\", Value: `-1`},\n\t\t{ID: 118, Key: \"plugin.status\", Value: `{}`},\n\t\t{ID: 119, Key: \"question.pin\", Value: `0`},\n\t\t{ID: 120, Key: \"question.unpin\", Value: `0`},\n\t\t{ID: 121, Key: \"question.show\", Value: `0`},\n\t\t{ID: 122, Key: \"question.hide\", Value: `0`},\n\t\t{ID: 123, Key: \"rank.question.pin\", Value: `-1`},\n\t\t{ID: 124, Key: \"rank.question.unpin\", Value: `-1`},\n\t\t{ID: 125, Key: \"rank.question.show\", Value: `-1`},\n\t\t{ID: 126, Key: \"rank.question.hide\", Value: `-1`},\n\t\t{ID: 127, Key: \"rank.answer.invite_someone_to_answer\", Value: `1000`},\n\t\t{ID: 128, Key: \"rank.answer.undeleted\", Value: `-1`},\n\t\t{ID: 129, Key: \"rank.question.undeleted\", Value: `-1`},\n\t\t{ID: 130, Key: \"rank.tag.undeleted\", Value: `-1`},\n\t\t{ID: 131, Key: \"ai_config.provider\", Value: `[{\"default_api_host\":\"https://api.openai.com\",\"display_name\":\"OpenAI\",\"name\":\"openai\"},{\"default_api_host\":\"https://generativelanguage.googleapis.com\",\"display_name\":\"Gemini\",\"name\":\"gemini\"},{\"default_api_host\":\"https://api.anthropic.com\",\"display_name\":\"Anthropic\",\"name\":\"anthropic\"}]`},\n\t}\n\n\tdefaultBadgeGroupTable = []*entity.BadgeGroup{\n\t\t{ID: \"1\", Name: \"badge.default_badge_groups.getting_started.name\"},\n\t\t{ID: \"2\", Name: \"badge.default_badge_groups.community.name\"},\n\t\t{ID: \"3\", Name: \"badge.default_badge_groups.posting.name\"},\n\t}\n\n\tdefaultBadgeTable = []*entity.Badge{\n\t\t{\n\t\t\tName:         \"badge.default_badges.autobiographer.name\",\n\t\t\tIcon:         \"person-badge-fill\",\n\t\t\tDescription:  \"badge.default_badges.autobiographer.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstUpdateUserProfile\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.editor.name\",\n\t\t\tIcon:         \"pencil-fill\",\n\t\t\tDescription:  \"badge.default_badges.editor.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstPostEdit\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.first_flag.name\",\n\t\t\tIcon:         \"flag-fill\",\n\t\t\tDescription:  \"badge.default_badges.first_flag.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstFlaggedPost\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.first_upvote.name\",\n\t\t\tIcon:         \"hand-thumbs-up-fill\",\n\t\t\tDescription:  \"badge.default_badges.first_upvote.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstVotedPost\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.first_reaction.name\",\n\t\t\tIcon:         \"emoji-smile-fill\",\n\t\t\tDescription:  \"badge.default_badges.first_reaction.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstReactedPost\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.first_share.name\",\n\t\t\tIcon:         \"share-fill\",\n\t\t\tDescription:  \"badge.default_badges.first_share.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstSharedPost\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.scholar.name\",\n\t\t\tIcon:         \"check-circle-fill\",\n\t\t\tDescription:  \"badge.default_badges.scholar.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 1,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"FirstAcceptAnswer\",\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.solved.name\",\n\t\t\tIcon:         \"check-square-fill\",\n\t\t\tDescription:  \"badge.default_badges.solved.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 2,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeSingleAward,\n\t\t\tHandler:      \"ReachAnswerAcceptedAmount\",\n\t\t\tParam:        `{\"amount\":\"1\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.nice_answer.name\",\n\t\t\tIcon:         \"chat-square-text-fill\",\n\t\t\tDescription:  \"badge.default_badges.nice_answer.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachAnswerVote\",\n\t\t\tParam:        `{\"amount\":\"10\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.good_answer.name\",\n\t\t\tIcon:         \"chat-square-text-fill\",\n\t\t\tDescription:  \"badge.default_badges.good_answer.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelSilver,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachAnswerVote\",\n\t\t\tParam:        `{\"amount\":\"25\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.great_answer.name\",\n\t\t\tIcon:         \"chat-square-text-fill\",\n\t\t\tDescription:  \"badge.default_badges.great_answer.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelGold,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachAnswerVote\",\n\t\t\tParam:        `{\"amount\":\"50\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.nice_question.name\",\n\t\t\tIcon:         \"question-circle-fill\",\n\t\t\tDescription:  \"badge.default_badges.nice_question.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelBronze,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachQuestionVote\",\n\t\t\tParam:        `{\"amount\":\"10\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.good_question.name\",\n\t\t\tIcon:         \"question-circle-fill\",\n\t\t\tDescription:  \"badge.default_badges.good_question.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelSilver,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachQuestionVote\",\n\t\t\tParam:        `{\"amount\":\"25\"}`,\n\t\t},\n\t\t{\n\t\t\tName:         \"badge.default_badges.great_question.name\",\n\t\t\tIcon:         \"question-circle-fill\",\n\t\t\tDescription:  \"badge.default_badges.great_question.desc\",\n\t\t\tStatus:       entity.BadgeStatusActive,\n\t\t\tBadgeGroupID: 3,\n\t\t\tLevel:        entity.BadgeLevelGold,\n\t\t\tSingle:       entity.BadgeMultiAward,\n\t\t\tHandler:      \"ReachQuestionVote\",\n\t\t\tParam:        `{\"amount\":\"50\"}`,\n\t\t},\n\t}\n)\n"
  },
  {
    "path": "internal/migrations/migrations.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nconst minDBVersion = 0\n\n// Migration describes on migration from lower version to high version\ntype Migration interface {\n\tVersion() string\n\tDescription() string\n\tMigrate(ctx context.Context, x *xorm.Engine) error\n\tShouldCleanCache() bool\n}\n\ntype migration struct {\n\tversion          string\n\tdescription      string\n\tmigrate          func(ctx context.Context, x *xorm.Engine) error\n\tshouldCleanCache bool\n}\n\n// Version returns the migration's version\nfunc (m *migration) Version() string {\n\treturn m.version\n}\n\n// Description returns the migration's description\nfunc (m *migration) Description() string {\n\treturn m.description\n}\n\n// Migrate executes the migration\nfunc (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error {\n\treturn m.migrate(ctx, x)\n}\n\n// ShouldCleanCache should clean the cache\nfunc (m *migration) ShouldCleanCache() bool {\n\treturn m.shouldCleanCache\n}\n\n// NewMigration creates a new migration\nfunc NewMigration(version, desc string, fn func(ctx context.Context, x *xorm.Engine) error, shouldCleanCache bool) Migration {\n\treturn &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache}\n}\n\n// Use noopMigration when there is a migration that has been no-oped\nvar noopMigration = func(_ context.Context, _ *xorm.Engine) error { return nil }\n\nvar migrations = []Migration{\n\t// 0->1\n\tNewMigration(\"v0.0.1\", \"this is first version, no operation\", noopMigration, false),\n\tNewMigration(\"v0.3.0\", \"add user language\", addUserLanguage, false),\n\tNewMigration(\"v0.4.1\", \"add recommend and reserved tag fields\", addTagRecommendedAndReserved, false),\n\tNewMigration(\"v0.5.0\", \"add activity timeline\", addActivityTimeline, false),\n\tNewMigration(\"v0.6.0\", \"add user role\", addRoleFeatures, false),\n\tNewMigration(\"v1.0.0\", \"add theme and private mode\", addThemeAndPrivateMode, true),\n\tNewMigration(\"v1.0.2\", \"add new answer notification\", addNewAnswerNotification, true),\n\tNewMigration(\"v1.0.5\", \"add plugin\", addPlugin, false),\n\tNewMigration(\"v1.0.7\", \"add user pin hide features\", addRolePinAndHideFeatures, true),\n\tNewMigration(\"v1.0.8\", \"update accept answer rank\", updateAcceptAnswerRank, true),\n\tNewMigration(\"v1.0.9\", \"add login limitations\", addLoginLimitations, true),\n\tNewMigration(\"v1.1.0-beta.1\", \"update user pin hide features\", updateRolePinAndHideFeatures, true),\n\tNewMigration(\"v1.1.0-beta.2\", \"update question post time\", updateQuestionPostTime, true),\n\tNewMigration(\"v1.1.0\", \"add gravatar base url\", updateCount, true),\n\tNewMigration(\"v1.1.1\", \"update the length of revision content\", updateTheLengthOfRevisionContent, false),\n\tNewMigration(\"v1.1.2\", \"add notification config\", addNoticeConfig, true),\n\tNewMigration(\"v1.1.3\", \"set default user notification config\", setDefaultUserNotificationConfig, false),\n\tNewMigration(\"v1.2.0\", \"add recover answer permission\", addRecoverPermission, true),\n\tNewMigration(\"v1.2.1\", \"add password login control\", addPasswordLoginControl, true),\n\tNewMigration(\"v1.2.5\", \"add notification plugin and theme config\", addNotificationPluginAndThemeConfig, true),\n\tNewMigration(\"v1.3.0\", \"add review\", addReview, false),\n\tNewMigration(\"v1.3.6\", \"add hot score to question table\", addQuestionHotScore, true),\n\tNewMigration(\"v1.4.0\", \"add badge/badge_group/badge_award table\", addBadges, true),\n\tNewMigration(\"v1.4.1\", \"add question link\", addQuestionLink, true),\n\tNewMigration(\"v1.4.2\", \"add the number of question links\", addQuestionLinkedCount, true),\n\tNewMigration(\"v1.4.5\", \"add file record\", addFileRecord, true),\n\tNewMigration(\"v1.5.1\", \"add plugin kv storage\", addPluginKVStorage, true),\n\tNewMigration(\"v1.6.0\", \"move user config to interface\", moveUserConfigToInterface, true),\n\tNewMigration(\"v1.7.0\", \"add optional tags\", addOptionalTags, true),\n\tNewMigration(\"v1.7.2\", \"expand avatar column length\", expandAvatarColumnLength, false),\n\tNewMigration(\"v1.8.0\", \"change admin menu\", updateAdminMenuSettings, true),\n\tNewMigration(\"v1.8.1\", \"ai feat\", aiFeat, true),\n}\n\nfunc GetMigrations() []Migration {\n\treturn migrations\n}\n\n// GetCurrentDBVersion returns the current db version\nfunc GetCurrentDBVersion(engine *xorm.Engine) (int64, error) {\n\tif err := engine.Sync(new(entity.Version)); err != nil {\n\t\treturn -1, fmt.Errorf(\"sync version failed: %v\", err)\n\t}\n\n\tcurrentVersion := &entity.Version{ID: 1}\n\thas, err := engine.Get(currentVersion)\n\tif err != nil {\n\t\treturn -1, fmt.Errorf(\"get first version failed: %v\", err)\n\t}\n\tif !has {\n\t\t_, err := engine.InsertOne(&entity.Version{ID: 1, VersionNumber: 0})\n\t\tif err != nil {\n\t\t\treturn -1, fmt.Errorf(\"insert first version failed: %v\", err)\n\t\t}\n\t\treturn 0, nil\n\t}\n\treturn currentVersion.VersionNumber, nil\n}\n\n// ExpectedVersion returns the expected db version\nfunc ExpectedVersion() int64 {\n\treturn int64(minDBVersion + len(migrations))\n}\n\n// Migrate database to current version\nfunc Migrate(debug bool, dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error {\n\tcache, cacheCleanup, err := data.NewCache(cacheConf)\n\tif err != nil {\n\t\tfmt.Println(\"new cache failed:\", err.Error())\n\t}\n\tengine, err := data.NewDB(debug, dbConf)\n\tif err != nil {\n\t\tfmt.Println(\"new database failed: \", err.Error())\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = engine.Close()\n\t}()\n\n\tcurrentDBVersion, err := GetCurrentDBVersion(engine)\n\tif err != nil {\n\t\treturn err\n\t}\n\texpectedVersion := ExpectedVersion()\n\tif len(upgradeToSpecificVersion) > 0 {\n\t\tfmt.Printf(\"[migrate] user set upgrade to version: %s\\n\", upgradeToSpecificVersion)\n\t\tfor i, m := range migrations {\n\t\t\tif m.Version() == upgradeToSpecificVersion {\n\t\t\t\tcurrentDBVersion = int64(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tfor currentDBVersion < expectedVersion {\n\t\tfmt.Printf(\"[migrate] current db version is %d, try to migrate version %d, latest version is %d\\n\",\n\t\t\tcurrentDBVersion, currentDBVersion+1, expectedVersion)\n\t\tmigrationFunc := migrations[currentDBVersion]\n\t\tfmt.Printf(\"[migrate] try to migrate Answer version %s, description: %s\\n\", migrationFunc.Version(), migrationFunc.Description())\n\t\tif err := migrationFunc.Migrate(context.Background(), engine); err != nil {\n\t\t\tfmt.Printf(\"[migrate] migrate to db version %d failed: %s\\n\", currentDBVersion+1, err.Error())\n\t\t\treturn err\n\t\t}\n\t\tif migrationFunc.ShouldCleanCache() {\n\t\t\tif err := cache.Flush(context.Background()); err != nil {\n\t\t\t\tfmt.Printf(\"[migrate] flush cache failed: %s\\n\", err.Error())\n\t\t\t}\n\t\t}\n\t\tfmt.Printf(\"[migrate] migrate to db version %d success\\n\", currentDBVersion+1)\n\t\tif _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {\n\t\t\tfmt.Printf(\"[migrate] migrate to db version %d, update failed: %s\", currentDBVersion+1, err.Error())\n\t\t\treturn err\n\t\t}\n\t\tcurrentDBVersion++\n\t}\n\tif cache != nil {\n\t\tcacheCleanup()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v1.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc addUserLanguage(ctx context.Context, x *xorm.Engine) error {\n\ttype User struct {\n\t\tID       string `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tUsername string `xorm:\"not null default '' VARCHAR(50) UNIQUE username\"`\n\t\tLanguage string `xorm:\"not null default '' VARCHAR(100) language\"`\n\t}\n\treturn x.Context(ctx).Sync(new(User))\n}\n"
  },
  {
    "path": "internal/migrations/v10.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/tidwall/gjson\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addLoginLimitations(ctx context.Context, x *xorm.Engine) error {\n\tloginSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeLogin,\n\t}\n\texist, err := x.Context(ctx).Get(loginSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\tcontent := &schema.SiteLoginReq{}\n\t\t_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)\n\t\tcontent.AllowEmailRegistrations = true\n\t\tcontent.AllowEmailDomains = make([]string, 0)\n\t\tdata, _ := json.Marshal(content)\n\t\tloginSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols(\"content\").Update(loginSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\n\tinterfaceSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeInterface,\n\t}\n\texist, err = x.Context(ctx).Get(interfaceSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tsiteUsers := &schema.SiteUsersReq{\n\t\tAllowUpdateDisplayName: true,\n\t\tAllowUpdateUsername:    true,\n\t\tAllowUpdateAvatar:      true,\n\t\tAllowUpdateBio:         true,\n\t\tAllowUpdateWebsite:     true,\n\t\tAllowUpdateLocation:    true,\n\t}\n\tif exist {\n\t\tsiteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, \"default_avatar\").String()\n\t}\n\tdata, _ := json.Marshal(siteUsers)\n\n\texist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\tusersSiteInfo := &entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeUsers,\n\t\t\tContent: string(data),\n\t\t\tStatus:  1,\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(usersSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"insert site info failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v11.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc updateRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error {\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 119, Key: \"question.pin\", Value: `0`},\n\t\t{ID: 120, Key: \"question.unpin\", Value: `0`},\n\t\t{ID: 121, Key: \"question.show\", Value: `0`},\n\t\t{ID: 122, Key: \"question.hide\", Value: `0`},\n\t\t{ID: 123, Key: \"rank.question.pin\", Value: `-1`},\n\t\t{ID: 124, Key: \"rank.question.unpin\", Value: `-1`},\n\t\t{ID: 125, Key: \"rank.question.show\", Value: `-1`},\n\t\t{ID: 126, Key: \"rank.question.hide\", Value: `-1`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v12.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\ntype QuestionPostTime struct {\n\tID               string    `xorm:\"not null pk BIGINT(20) id\"`\n\tCreatedAt        time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt        time.Time `xorm:\"updated_at TIMESTAMP\"`\n\tUserID           string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tLastEditUserID   string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\tTitle            string    `xorm:\"not null default '' VARCHAR(150) title\"`\n\tOriginalText     string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText       string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\tStatus           int       `xorm:\"not null default 1 INT(11) status\"`\n\tPin              int       `xorm:\"not null default 1 INT(11) pin\"`\n\tShow             int       `xorm:\"not null default 1 INT(11) show\"`\n\tViewCount        int       `xorm:\"not null default 0 INT(11) view_count\"`\n\tUniqueViewCount  int       `xorm:\"not null default 0 INT(11) unique_view_count\"`\n\tVoteCount        int       `xorm:\"not null default 0 INT(11) vote_count\"`\n\tAnswerCount      int       `xorm:\"not null default 0 INT(11) answer_count\"`\n\tCollectionCount  int       `xorm:\"not null default 0 INT(11) collection_count\"`\n\tFollowCount      int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\tAcceptedAnswerID string    `xorm:\"not null default 0 BIGINT(20) accepted_answer_id\"`\n\tLastAnswerID     string    `xorm:\"not null default 0 BIGINT(20) last_answer_id\"`\n\tPostUpdateTime   time.Time `xorm:\"post_update_time TIMESTAMP\"`\n\tRevisionID       string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n}\n\nfunc (QuestionPostTime) TableName() string {\n\treturn \"question\"\n}\n\nfunc updateQuestionPostTime(ctx context.Context, x *xorm.Engine) error {\n\tquestionList := make([]QuestionPostTime, 0)\n\terr := x.Context(ctx).Find(&questionList, &entity.Question{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get questions failed: %w\", err)\n\t}\n\tfor _, item := range questionList {\n\t\tif item.PostUpdateTime.IsZero() {\n\t\t\tif !item.UpdatedAt.IsZero() {\n\t\t\t\titem.PostUpdateTime = item.UpdatedAt\n\t\t\t} else if !item.CreatedAt.IsZero() {\n\t\t\t\titem.PostUpdateTime = item.CreatedAt\n\t\t\t}\n\t\t\tif _, err = x.Context(ctx).Update(item, &QuestionPostTime{ID: item.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", item, err)\n\t\t\t\treturn fmt.Errorf(\"update question failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v13.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc updateCount(ctx context.Context, x *xorm.Engine) error {\n\tfns := []func(ctx context.Context, x *xorm.Engine) error{\n\t\tinviteAnswer,\n\t\taddPrivilegeForInviteSomeoneToAnswer,\n\t\taddGravatarBaseURL,\n\t\tupdateQuestionCount,\n\t\tupdateTagCount,\n\t\tupdateUserQuestionCount,\n\t\tupdateUserAnswerCount,\n\t\tinBoxData,\n\t}\n\tfor _, fn := range fns {\n\t\tif err := fn(ctx, x); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc addGravatarBaseURL(ctx context.Context, x *xorm.Engine) error {\n\tusersSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeUsers,\n\t}\n\texist, err := x.Context(ctx).Get(usersSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\tcontent := &schema.SiteUsersReq{}\n\t\t_ = json.Unmarshal([]byte(usersSiteInfo.Content), content)\n\t\tcontent.GravatarBaseURL = \"https://www.gravatar.com/avatar/\"\n\t\tdata, _ := json.Marshal(content)\n\t\tusersSiteInfo.Content = string(data)\n\n\t\t_, err = x.Context(ctx).ID(usersSiteInfo.ID).Cols(\"content\").Update(usersSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc addPrivilegeForInviteSomeoneToAnswer(ctx context.Context, x *xorm.Engine) error {\n\t// add rank for invite to answer\n\tpowers := []*entity.Power{\n\t\t{ID: 38, Name: \"invite someone to answer\", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: \"invite someone to answer\"},\n\t}\n\tfor _, power := range powers {\n\t\texist, err := x.Context(ctx).Get(&entity.Power{PowerType: power.PowerType})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\t_, err = x.Context(ctx).ID(power.ID).Update(power)\n\t\t} else {\n\t\t\t_, err = x.Context(ctx).Insert(power)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\trolePowerRels := []*entity.RolePowerRel{\n\t\t{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},\n\t\t{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},\n\t}\n\tfor _, rel := range rolePowerRels {\n\t\texist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(rel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 127, Key: \"rank.answer.invite_someone_to_answer\", Value: `1000`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc updateQuestionCount(ctx context.Context, x *xorm.Engine) error {\n\t// question answer count\n\tanswers := make([]AnswerV13, 0)\n\terr := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get answers failed: %w\", err)\n\t}\n\tquestionAnswerCount := make(map[string]int)\n\tfor _, answer := range answers {\n\t\t_, ok := questionAnswerCount[answer.QuestionID]\n\t\tif !ok {\n\t\t\tquestionAnswerCount[answer.QuestionID] = 1\n\t\t} else {\n\t\t\tquestionAnswerCount[answer.QuestionID]++\n\t\t}\n\t}\n\tquestionList := make([]QuestionV13, 0)\n\terr = x.Context(ctx).Find(&questionList, &QuestionV13{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get questions failed: %w\", err)\n\t}\n\tfor _, item := range questionList {\n\t\t_, ok := questionAnswerCount[item.ID]\n\t\tif ok {\n\t\t\titem.AnswerCount = questionAnswerCount[item.ID]\n\t\t\tif _, err = x.Context(ctx).Cols(\"answer_count\").Update(item, &QuestionV13{ID: item.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", item, err)\n\t\t\t\treturn fmt.Errorf(\"update question failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// updateTagCount update tag count\nfunc updateTagCount(ctx context.Context, x *xorm.Engine) error {\n\ttagRelList := make([]entity.TagRel, 0)\n\terr := x.Context(ctx).Find(&tagRelList, &entity.TagRel{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get tag rel failed: %w\", err)\n\t}\n\tquestionIDs := make([]string, 0)\n\tquestionsAvailableMap := make(map[string]bool)\n\tquestionsHideMap := make(map[string]bool)\n\tfor _, item := range tagRelList {\n\t\tquestionIDs = append(questionIDs, item.ObjectID)\n\t\tquestionsAvailableMap[item.ObjectID] = false\n\t\tquestionsHideMap[item.ObjectID] = false\n\t}\n\tquestionList := make([]QuestionV13, 0)\n\terr = x.Context(ctx).In(\"id\", questionIDs).And(builder.Lt{\"question.status\": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get questions failed: %w\", err)\n\t}\n\tfor _, question := range questionList {\n\t\t_, ok := questionsAvailableMap[question.ID]\n\t\tif ok {\n\t\t\tquestionsAvailableMap[question.ID] = true\n\t\t\tif question.Show == entity.QuestionHide {\n\t\t\t\tquestionsHideMap[question.ID] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor id, ok := range questionsHideMap {\n\t\tif ok {\n\t\t\tif _, err = x.Context(ctx).Cols(\"status\").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", id, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor id, ok := range questionsAvailableMap {\n\t\tif !ok {\n\t\t\tif _, err = x.Context(ctx).Cols(\"status\").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", id, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// select tag count\n\tnewTagRelList := make([]entity.TagRel, 0)\n\terr = x.Context(ctx).Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get tag rel failed: %w\", err)\n\t}\n\ttagCountMap := make(map[string]int)\n\tfor _, v := range newTagRelList {\n\t\t_, ok := tagCountMap[v.TagID]\n\t\tif !ok {\n\t\t\ttagCountMap[v.TagID] = 1\n\t\t} else {\n\t\t\ttagCountMap[v.TagID]++\n\t\t}\n\t}\n\tTagList := make([]entity.Tag, 0)\n\terr = x.Context(ctx).Find(&TagList, &entity.Tag{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get tag  failed: %w\", err)\n\t}\n\tfor _, tag := range TagList {\n\t\t_, ok := tagCountMap[tag.ID]\n\t\tif ok {\n\t\t\ttag.QuestionCount = tagCountMap[tag.ID]\n\t\t\tif _, err = x.Context(ctx).Update(tag, &entity.Tag{ID: tag.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v tag failed: %s\", tag.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update tag failed: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\ttag.QuestionCount = 0\n\t\t\tif _, err = x.Context(ctx).Cols(\"question_count\").Update(tag, &entity.Tag{ID: tag.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v tag failed: %s\", tag.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update tag failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// updateUserQuestionCount update user question count\nfunc updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error {\n\tquestionList := make([]QuestionV13, 0)\n\terr := x.Context(ctx).Where(builder.Lt{\"status\": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get question  failed: %w\", err)\n\t}\n\tuserQuestionCountMap := make(map[string]int)\n\tfor _, question := range questionList {\n\t\t_, ok := userQuestionCountMap[question.UserID]\n\t\tif !ok {\n\t\t\tuserQuestionCountMap[question.UserID] = 1\n\t\t} else {\n\t\t\tuserQuestionCountMap[question.UserID]++\n\t\t}\n\t}\n\tuserList := make([]entity.User, 0)\n\terr = x.Context(ctx).Find(&userList, &entity.User{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get user  failed: %w\", err)\n\t}\n\tfor _, user := range userList {\n\t\t_, ok := userQuestionCountMap[user.ID]\n\t\tif ok {\n\t\t\tuser.QuestionCount = userQuestionCountMap[user.ID]\n\t\t\tif _, err = x.Context(ctx).Cols(\"question_count\").Update(user, &entity.User{ID: user.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v user failed: %s\", user.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update user failed: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tuser.QuestionCount = 0\n\t\t\tif _, err = x.Context(ctx).Cols(\"question_count\").Update(user, &entity.User{ID: user.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v user failed: %s\", user.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update user failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype AnswerV13 struct {\n\tID         string `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tQuestionID string `xorm:\"not null default 0 BIGINT(20) question_id\"`\n\tUserID     string `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tStatus     int    `xorm:\"not null default 1 INT(11) status\"`\n\tAccepted   int    `xorm:\"not null default 1 INT(11) adopted\"`\n}\n\nfunc (AnswerV13) TableName() string {\n\treturn \"answer\"\n}\n\n// updateUserAnswerCount update user answer count\nfunc updateUserAnswerCount(ctx context.Context, x *xorm.Engine) error {\n\tanswers := make([]AnswerV13, 0)\n\terr := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get answers failed: %w\", err)\n\t}\n\tuserAnswerCount := make(map[string]int)\n\tfor _, answer := range answers {\n\t\t_, ok := userAnswerCount[answer.UserID]\n\t\tif !ok {\n\t\t\tuserAnswerCount[answer.UserID] = 1\n\t\t} else {\n\t\t\tuserAnswerCount[answer.UserID]++\n\t\t}\n\t}\n\tuserList := make([]entity.User, 0)\n\terr = x.Context(ctx).Find(&userList, &entity.User{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get user failed: %w\", err)\n\t}\n\tfor _, user := range userList {\n\t\t_, ok := userAnswerCount[user.ID]\n\t\tif ok {\n\t\t\tuser.AnswerCount = userAnswerCount[user.ID]\n\t\t\tif _, err = x.Context(ctx).Cols(\"answer_count\").Update(user, &entity.User{ID: user.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v user failed: %s\", user.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update user failed: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tuser.AnswerCount = 0\n\t\t\tif _, err = x.Context(ctx).Cols(\"answer_count\").Update(user, &entity.User{ID: user.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v user failed: %s\", user.ID, err)\n\t\t\t\treturn fmt.Errorf(\"update user failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype QuestionV13 struct {\n\tID               string    `xorm:\"not null pk BIGINT(20) id\"`\n\tCreatedAt        time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\tUpdatedAt        time.Time `xorm:\"updated_at TIMESTAMP\"`\n\tUserID           string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\tInviteUserID     string    `xorm:\"TEXT invite_user_id\"`\n\tLastEditUserID   string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\tTitle            string    `xorm:\"not null default '' VARCHAR(150) title\"`\n\tOriginalText     string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\tParsedText       string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\tStatus           int       `xorm:\"not null default 1 INT(11) status\"`\n\tPin              int       `xorm:\"not null default 1 INT(11) pin\"`\n\tShow             int       `xorm:\"not null default 1 INT(11) show\"`\n\tViewCount        int       `xorm:\"not null default 0 INT(11) view_count\"`\n\tUniqueViewCount  int       `xorm:\"not null default 0 INT(11) unique_view_count\"`\n\tVoteCount        int       `xorm:\"not null default 0 INT(11) vote_count\"`\n\tAnswerCount      int       `xorm:\"not null default 0 INT(11) answer_count\"`\n\tCollectionCount  int       `xorm:\"not null default 0 INT(11) collection_count\"`\n\tFollowCount      int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\tAcceptedAnswerID string    `xorm:\"not null default 0 BIGINT(20) accepted_answer_id\"`\n\tLastAnswerID     string    `xorm:\"not null default 0 BIGINT(20) last_answer_id\"`\n\tPostUpdateTime   time.Time `xorm:\"post_update_time TIMESTAMP\"`\n\tRevisionID       string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n}\n\nfunc (QuestionV13) TableName() string {\n\treturn \"question\"\n}\n\nfunc inviteAnswer(ctx context.Context, x *xorm.Engine) error {\n\terr := x.Context(ctx).Sync(new(QuestionV13))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// inBoxData Classify messages\nfunc inBoxData(ctx context.Context, x *xorm.Engine) error {\n\ttype Notification struct {\n\t\tID        string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tCreatedAt time.Time `xorm:\"created TIMESTAMP created_at\"`\n\t\tUpdatedAt time.Time `xorm:\"TIMESTAMP updated_at\"`\n\t\tUserID    string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\t\tObjectID  string    `xorm:\"not null default 0 INDEX BIGINT(20) object_id\"`\n\t\tContent   string    `xorm:\"not null TEXT content\"`\n\t\tType      int       `xorm:\"not null default 0 INT(11) type\"`\n\t\tMsgType   int       `xorm:\"not null default 0 INT(11) msg_type\"`\n\t\tIsRead    int       `xorm:\"not null default 1 INT(11) is_read\"`\n\t\tStatus    int       `xorm:\"not null default 1 INT(11) status\"`\n\t}\n\terr := x.Context(ctx).Sync(new(Notification))\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsglist := make([]entity.Notification, 0)\n\terr = x.Context(ctx).Find(&msglist, &entity.Notification{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get Notification failed: %w\", err)\n\t}\n\tfor _, v := range msglist {\n\t\tContent := &schema.NotificationContent{}\n\t\terr := json.Unmarshal([]byte(v.Content), Content)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\t_, ok := constant.NotificationMsgTypeMapping[Content.NotificationAction]\n\t\tif ok {\n\t\t\tv.MsgType = constant.NotificationMsgTypeMapping[Content.NotificationAction]\n\t\t\tif _, err = x.Context(ctx).Update(v, &entity.Notification{ID: v.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v Notification failed: %s\", v.ID, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v14.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"xorm.io/xorm/schemas\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc updateTheLengthOfRevisionContent(ctx context.Context, x *xorm.Engine) (err error) {\n\tsess := x.Context(ctx)\n\tif x.Dialect().URI().DBType == schemas.MYSQL {\n\t\t_, err = sess.Exec(\"ALTER TABLE `revision` CHANGE `content` `content` MEDIUMTEXT NOT NULL;\")\n\t}\n\treturn err\n}\n\ntype RevisionV14 struct {\n\tID           string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\tCreatedAt    time.Time `xorm:\"created TIMESTAMP created_at\"`\n\tUpdatedAt    time.Time `xorm:\"updated TIMESTAMP updated_at\"`\n\tUserID       string    `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\tObjectType   int       `xorm:\"not null default 0 INT(11) object_type\"`\n\tObjectID     string    `xorm:\"not null default 0 BIGINT(20) INDEX object_id\"`\n\tTitle        string    `xorm:\"not null default '' VARCHAR(255) title\"`\n\tContent      string    `xorm:\"not null MEDIUMTEXT content\"`\n\tLog          string    `xorm:\"VARCHAR(255) log\"`\n\tStatus       int       `xorm:\"not null default 1 INT(11) status\"`\n\tReviewUserID int64     `xorm:\"not null default 0 BIGINT(20) review_user_id\"`\n}\n\nfunc (RevisionV14) TableName() string {\n\treturn \"revision\"\n}\n"
  },
  {
    "path": "internal/migrations/v15.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addNoticeConfig(ctx context.Context, x *xorm.Engine) error {\n\treturn x.Context(ctx).Sync(new(entity.UserNotificationConfig))\n}\n"
  },
  {
    "path": "internal/migrations/v16.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc setDefaultUserNotificationConfig(ctx context.Context, x *xorm.Engine) error {\n\tuserIDs := make([]string, 0)\n\terr := x.Context(ctx).Table(\"user\").Select(\"id\").Find(&userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, id := range userIDs {\n\t\tbean := entity.UserNotificationConfig{UserID: id, Source: string(constant.InboxSource)}\n\t\texist, err := x.Context(ctx).Get(&bean)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(&entity.UserNotificationConfig{\n\t\t\tUserID:   id,\n\t\t\tSource:   string(constant.InboxSource),\n\t\t\tChannels: `[{\"key\":\"email\",\"enable\":true}]`,\n\t\t\tEnabled:  true,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v17.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addRecoverPermission(ctx context.Context, x *xorm.Engine) error {\n\tpowers := []*entity.Power{\n\t\t{ID: 39, Name: \"recover answer\", PowerType: permission.AnswerUnDelete, Description: \"recover deleted answer\"},\n\t\t{ID: 40, Name: \"recover question\", PowerType: permission.QuestionUnDelete, Description: \"recover deleted question\"},\n\t\t{ID: 41, Name: \"recover tag\", PowerType: permission.TagUnDelete, Description: \"recover deleted tag\"},\n\t}\n\tfor _, power := range powers {\n\t\texist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\t_, err = x.Context(ctx).ID(power.ID).Update(power)\n\t\t} else {\n\t\t\t_, err = x.Context(ctx).Insert(power)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trolePowerRels := []*entity.RolePowerRel{\n\t\t{RoleID: 2, PowerType: permission.AnswerUnDelete},\n\t\t{RoleID: 2, PowerType: permission.QuestionUnDelete},\n\t\t{RoleID: 2, PowerType: permission.TagUnDelete},\n\n\t\t{RoleID: 3, PowerType: permission.AnswerUnDelete},\n\t\t{RoleID: 3, PowerType: permission.QuestionUnDelete},\n\t\t{RoleID: 3, PowerType: permission.TagUnDelete},\n\t}\n\tfor _, rel := range rolePowerRels {\n\t\texist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(rel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 128, Key: \"rank.answer.undeleted\", Value: `-1`},\n\t\t{ID: 129, Key: \"rank.question.undeleted\", Value: `-1`},\n\t\t{ID: 130, Key: \"rank.tag.undeleted\", Value: `-1`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v18.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addPasswordLoginControl(ctx context.Context, x *xorm.Engine) error {\n\tloginSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeLogin,\n\t}\n\texist, err := x.Context(ctx).Get(loginSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\tcontent := &schema.SiteLoginReq{}\n\t\t_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)\n\t\tcontent.AllowPasswordLogin = true\n\t\tdata, _ := json.Marshal(content)\n\t\tloginSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols(\"content\").Update(loginSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\n\twriteSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeWrite,\n\t}\n\texist, err = x.Context(ctx).Get(writeSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\ttype OldSiteWriteReq struct {\n\t\t\tRestrictAnswer bool     `validate:\"omitempty\" form:\"restrict_answer\" json:\"restrict_answer\"`\n\t\t\tRequiredTag    bool     `validate:\"omitempty\" form:\"required_tag\" json:\"required_tag\"`\n\t\t\tRecommendTags  []string `validate:\"omitempty\" form:\"recommend_tags\" json:\"recommend_tags\"`\n\t\t\tReservedTags   []string `validate:\"omitempty\" form:\"reserved_tags\" json:\"reserved_tags\"`\n\t\t\tUserID         string   `json:\"-\"`\n\t\t}\n\t\tcontent := &OldSiteWriteReq{}\n\t\t_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)\n\t\tcontent.RestrictAnswer = true\n\t\tdata, _ := json.Marshal(content)\n\t\twriteSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols(\"content\").Update(writeSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\n\ttype User struct {\n\t\tAvatar string `xorm:\"not null default '' VARCHAR(1024) avatar\"`\n\t}\n\treturn x.Context(ctx).Sync(new(User))\n}\n"
  },
  {
    "path": "internal/migrations/v19.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addNotificationPluginAndThemeConfig(ctx context.Context, x *xorm.Engine) error {\n\ttype User struct {\n\t\tID          string `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tColorScheme string `xorm:\"not null default '' VARCHAR(100) color_scheme\"`\n\t}\n\treturn x.Context(ctx).Sync(new(entity.PluginUserConfig), new(User))\n}\n"
  },
  {
    "path": "internal/migrations/v2.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc addTagRecommendedAndReserved(ctx context.Context, x *xorm.Engine) error {\n\ttype Tag struct {\n\t\tID        string `xorm:\"not null pk comment('tag_id') BIGINT(20) id\"`\n\t\tSlugName  string `xorm:\"not null default '' unique VARCHAR(35) slug_name\"`\n\t\tRecommend bool   `xorm:\"not null default false BOOL recommend\"`\n\t\tReserved  bool   `xorm:\"not null default false BOOL reserved\"`\n\t}\n\treturn x.Context(ctx).Sync(new(Tag))\n}\n"
  },
  {
    "path": "internal/migrations/v20.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addReview(ctx context.Context, x *xorm.Engine) error {\n\tc := &entity.Config{Key: \"reason.not_clarity\", Value: `{\"name\":\"needs details or clarity\",\"description\":\"This question currently includes multiple questions in one. It should focus on one problem only.\"}`}\n\tif _, err := x.Context(ctx).Update(c, &entity.Config{Key: \"reason.not_clarity\"}); err != nil {\n\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t}\n\treturn x.Context(ctx).Sync(new(entity.Review))\n}\n"
  },
  {
    "path": "internal/migrations/v21.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc addQuestionHotScore(ctx context.Context, x *xorm.Engine) error {\n\ttype Question struct {\n\t\tHotScore int `xorm:\"not null default 0 INT(11) hot_score\"`\n\t}\n\treturn x.Context(ctx).Sync(new(Question))\n}\n"
  },
  {
    "path": "internal/migrations/v22.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addBadges(ctx context.Context, x *xorm.Engine) (err error) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x})\n\n\terr = x.Context(ctx).Sync(new(entity.Badge), new(entity.BadgeGroup), new(entity.BadgeAward))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sync table failed: %w\", err)\n\t}\n\n\tfor _, badgeGroup := range defaultBadgeGroupTable {\n\t\texist, err := x.Context(ctx).Get(&entity.BadgeGroup{ID: badgeGroup.ID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\t_, err = x.Context(ctx).ID(badgeGroup.ID).Update(badgeGroup)\n\t\t} else {\n\t\t\t_, err = x.Context(ctx).Insert(badgeGroup)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"insert badge group failed: %w\", err)\n\t\t}\n\t}\n\n\tfor _, badge := range defaultBadgeTable {\n\t\tbeans := &entity.Badge{Name: badge.Name}\n\t\texist, err := x.Context(ctx).Get(beans)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tbadge.ID = beans.ID\n\t\t\t_, err = x.Context(ctx).ID(beans.ID).Update(badge)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"update badge failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tbadge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif _, err := x.Context(ctx).Insert(badge); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/migrations/v23.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addQuestionLink(ctx context.Context, x *xorm.Engine) (err error) {\n\treturn x.Context(ctx).Sync(new(entity.QuestionLink))\n}\n"
  },
  {
    "path": "internal/migrations/v24.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc addQuestionLinkedCount(ctx context.Context, x *xorm.Engine) error {\n\twriteSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeWrite,\n\t}\n\texist, err := x.Context(ctx).Get(writeSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\ttype OldSiteWriteReq struct {\n\t\t\tRestrictAnswer                 bool                   `json:\"restrict_answer\"`\n\t\t\tRequiredTag                    bool                   `json:\"required_tag\"`\n\t\t\tRecommendTags                  []*schema.SiteWriteTag `json:\"recommend_tags\"`\n\t\t\tReservedTags                   []*schema.SiteWriteTag `json:\"reserved_tags\"`\n\t\t\tMaxImageSize                   int                    `json:\"max_image_size\"`\n\t\t\tMaxAttachmentSize              int                    `json:\"max_attachment_size\"`\n\t\t\tMaxImageMegapixel              int                    `json:\"max_image_megapixel\"`\n\t\t\tAuthorizedImageExtensions      []string               `json:\"authorized_image_extensions\"`\n\t\t\tAuthorizedAttachmentExtensions []string               `json:\"authorized_attachment_extensions\"`\n\t\t}\n\t\tcontent := &OldSiteWriteReq{}\n\t\t_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)\n\t\tcontent.MaxImageSize = 4\n\t\tcontent.MaxAttachmentSize = 8\n\t\tcontent.MaxImageMegapixel = 40\n\t\tcontent.AuthorizedImageExtensions = []string{\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\"}\n\t\tcontent.AuthorizedAttachmentExtensions = []string{}\n\t\tdata, _ := json.Marshal(content)\n\t\twriteSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols(\"content\").Update(writeSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\n\treturn x.Context(ctx).Sync(new(entity.Question))\n}\n"
  },
  {
    "path": "internal/migrations/v25.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addFileRecord(ctx context.Context, x *xorm.Engine) error {\n\tif err := x.Context(ctx).Sync(new(entity.FileRecord)); err != nil {\n\t\treturn err\n\t}\n\n\t// Set default external_content_display to always_display\n\tlegalInfo := &entity.SiteInfo{Type: \"legal\"}\n\texist, err := x.Context(ctx).Get(legalInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get legal config failed: %w\", err)\n\t}\n\tlegalConfig := make(map[string]any)\n\tif exist {\n\t\tif err := json.Unmarshal([]byte(legalInfo.Content), &legalConfig); err != nil {\n\t\t\treturn fmt.Errorf(\"unmarshal legal config failed: %w\", err)\n\t\t}\n\t}\n\tlegalConfig[\"external_content_display\"] = \"always_display\"\n\tlegalConfigBytes, _ := json.Marshal(legalConfig)\n\tif exist {\n\t\tlegalInfo.Content = string(legalConfigBytes)\n\t\t_, err = x.Context(ctx).ID(legalInfo.ID).Cols(\"content\").Update(legalInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update legal config failed: %w\", err)\n\t\t}\n\t} else {\n\t\tlegalInfo.Content = string(legalConfigBytes)\n\t\tlegalInfo.Status = 1\n\t\t_, err = x.Context(ctx).Insert(legalInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"insert legal config failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v26.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addPluginKVStorage(ctx context.Context, x *xorm.Engine) error {\n\treturn x.Context(ctx).Sync(new(entity.PluginKVStorage))\n}\n"
  },
  {
    "path": "internal/migrations/v27.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addSuspendedUntilToUser(ctx context.Context, x *xorm.Engine) error {\n\ttype User struct {\n\t\tSuspendedUntil *time.Time `xorm:\"DATETIME suspended_until\"`\n\t}\n\treturn x.Context(ctx).Sync(new(User))\n}\n\nfunc moveUserConfigToInterface(ctx context.Context, x *xorm.Engine) error {\n\tif err := addSuspendedUntilToUser(ctx, x); err != nil {\n\t\treturn fmt.Errorf(\"add suspended_until to user failed: %w\", err)\n\t}\n\n\t// Get old interface config\n\tinterfaceSiteInfo := &entity.SiteInfo{Type: constant.SiteTypeInterface}\n\texist, err := x.Context(ctx).Get(interfaceSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\treturn fmt.Errorf(\"interface site info not found\")\n\t}\n\n\tinterfaceConfig := &schema.SiteInterfaceReq{}\n\t_ = json.Unmarshal([]byte(interfaceSiteInfo.Content), interfaceConfig)\n\n\t// Get old user config\n\tusersConfig := &entity.SiteInfo{Type: constant.SiteTypeUsers}\n\texist, err = x.Context(ctx).Get(usersConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\treturn fmt.Errorf(\"users site info not found\")\n\t}\n\n\tsiteUsers := &schema.SiteUsersReq{}\n\t_ = json.Unmarshal([]byte(usersConfig.Content), siteUsers)\n\n\tinterfaceConfig.DefaultAvatar = siteUsers.DefaultAvatar\n\tinterfaceConfig.GravatarBaseURL = siteUsers.GravatarBaseURL\n\n\tinterfaceConfigByte, _ := json.Marshal(interfaceConfig)\n\tinterfaceSiteInfo.Content = string(interfaceConfigByte)\n\n\t_, err = x.Context(ctx).ID(interfaceSiteInfo.ID).Update(interfaceSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"insert site info failed: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v28.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc addOptionalTags(ctx context.Context, x *xorm.Engine) error {\n\twriteSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeWrite,\n\t}\n\texist, err := x.Context(ctx).Get(writeSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\ttype OldSiteWriteReq struct {\n\t\t\tMinimumContent                 int                    `json:\"min_content\"`\n\t\t\tRestrictAnswer                 bool                   `json:\"restrict_answer\"`\n\t\t\tMinimumTags                    int                    `json:\"min_tags\"`\n\t\t\tRequiredTag                    bool                   `json:\"required_tag\"`\n\t\t\tRecommendTags                  []*schema.SiteWriteTag `json:\"recommend_tags\"`\n\t\t\tReservedTags                   []*schema.SiteWriteTag `json:\"reserved_tags\"`\n\t\t\tMaxImageSize                   int                    `json:\"max_image_size\"`\n\t\t\tMaxAttachmentSize              int                    `json:\"max_attachment_size\"`\n\t\t\tMaxImageMegapixel              int                    `json:\"max_image_megapixel\"`\n\t\t\tAuthorizedImageExtensions      []string               `json:\"authorized_image_extensions\"`\n\t\t\tAuthorizedAttachmentExtensions []string               `json:\"authorized_attachment_extensions\"`\n\t\t}\n\t\tcontent := &OldSiteWriteReq{}\n\t\t_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)\n\t\tcontent.MinimumTags = 1\n\t\tcontent.MinimumContent = 6\n\t\tdata, _ := json.Marshal(content)\n\t\twriteSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols(\"content\").Update(writeSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v29.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"xorm.io/xorm\"\n)\n\nfunc expandAvatarColumnLength(ctx context.Context, x *xorm.Engine) error {\n\ttype User struct {\n\t\tAvatar string `xorm:\"not null default '' VARCHAR(2048) avatar\"`\n\t}\n\tif err := x.Context(ctx).Sync(new(User)); err != nil {\n\t\treturn fmt.Errorf(\"expand avatar column length failed: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v3.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n\t\"xorm.io/xorm/schemas\"\n)\n\nfunc addActivityTimeline(ctx context.Context, x *xorm.Engine) (err error) {\n\tswitch x.Dialect().URI().DBType {\n\tcase schemas.MYSQL:\n\t\t_, err = x.Context(ctx).Exec(\"ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = x.Context(ctx).Exec(\"ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase schemas.POSTGRES:\n\t\t_, err = x.Context(ctx).Exec(`ALTER TABLE \"answer\" ALTER COLUMN \"updated_at\" DROP NOT NULL, ALTER COLUMN \"updated_at\" SET DEFAULT NULL`)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = x.Context(ctx).Exec(`ALTER TABLE \"question\" ALTER COLUMN \"updated_at\" DROP NOT NULL, ALTER COLUMN \"updated_at\" SET DEFAULT NULL`)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase schemas.SQLITE:\n\t\t_, err = x.Context(ctx).Exec(`DROP INDEX \"IDX_answer_user_id\";\n\nALTER TABLE \"answer\" RENAME TO \"_answer_old_v3\";\n\nCREATE TABLE \"answer\" (\n  \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n  \"created_at\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  \"updated_at\" DATETIME DEFAULT NULL,\n  \"question_id\" INTEGER NOT NULL DEFAULT 0,\n  \"user_id\" INTEGER NOT NULL DEFAULT 0,\n  \"original_text\" TEXT NOT NULL,\n  \"parsed_text\" TEXT NOT NULL,\n  \"status\" INTEGER NOT NULL DEFAULT 1,\n  \"adopted\" INTEGER NOT NULL DEFAULT 1,\n  \"comment_count\" INTEGER NOT NULL DEFAULT 0,\n  \"vote_count\" INTEGER NOT NULL DEFAULT 0,\n  \"revision_id\" INTEGER NOT NULL DEFAULT 0\n);\n\nINSERT INTO \"answer\" (\"id\", \"created_at\", \"updated_at\", \"question_id\", \"user_id\", \"original_text\", \"parsed_text\", \"status\", \"adopted\", \"comment_count\", \"vote_count\", \"revision_id\") SELECT \"id\", \"created_at\", \"updated_at\", \"question_id\", \"user_id\", \"original_text\", \"parsed_text\", \"status\", \"adopted\", \"comment_count\", \"vote_count\", \"revision_id\" FROM \"_answer_old_v3\";\n\nCREATE INDEX \"IDX_answer_user_id\"\nON \"answer\" (\n  \"user_id\" ASC\n);\nDROP INDEX \"IDX_question_user_id\";\n\nALTER TABLE \"question\" RENAME TO \"_question_old_v3\";\n\nCREATE TABLE \"question\" (\n  \"id\" INTEGER NOT NULL,\n  \"created_at\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  \"updated_at\" DATETIME DEFAULT NULL,\n  \"user_id\" INTEGER NOT NULL DEFAULT 0,\n  \"title\" TEXT NOT NULL DEFAULT '',\n  \"original_text\" TEXT NOT NULL,\n  \"parsed_text\" TEXT NOT NULL,\n  \"status\" INTEGER NOT NULL DEFAULT 1,\n  \"view_count\" INTEGER NOT NULL DEFAULT 0,\n  \"unique_view_count\" INTEGER NOT NULL DEFAULT 0,\n  \"vote_count\" INTEGER NOT NULL DEFAULT 0,\n  \"answer_count\" INTEGER NOT NULL DEFAULT 0,\n  \"collection_count\" INTEGER NOT NULL DEFAULT 0,\n  \"follow_count\" INTEGER NOT NULL DEFAULT 0,\n  \"accepted_answer_id\" INTEGER NOT NULL DEFAULT 0,\n  \"last_answer_id\" INTEGER NOT NULL DEFAULT 0,\n  \"post_update_time\" DATETIME DEFAULT CURRENT_TIMESTAMP,\n  \"revision_id\" INTEGER NOT NULL DEFAULT 0,\n  PRIMARY KEY (\"id\")\n);\n\nINSERT INTO \"question\" (\"id\", \"created_at\", \"updated_at\", \"user_id\", \"title\", \"original_text\", \"parsed_text\", \"status\", \"view_count\", \"unique_view_count\", \"vote_count\", \"answer_count\", \"collection_count\", \"follow_count\", \"accepted_answer_id\", \"last_answer_id\", \"post_update_time\", \"revision_id\") SELECT \"id\", \"created_at\", \"updated_at\", \"user_id\", \"title\", \"original_text\", \"parsed_text\", \"status\", \"view_count\", \"unique_view_count\", \"vote_count\", \"answer_count\", \"collection_count\", \"follow_count\", \"accepted_answer_id\", \"last_answer_id\", \"post_update_time\", \"revision_id\" FROM \"_question_old_v3\";\n\nCREATE INDEX \"IDX_question_user_id\"\nON \"question\" (\n  \"user_id\" ASC\n);`)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// only increasing field length to 128\n\ttype Config struct {\n\t\tID  int    `xorm:\"not null pk autoincr INT(11) id\"`\n\t\tKey string `xorm:\"unique VARCHAR(128) key\"`\n\t}\n\tif err := x.Context(ctx).Sync(new(Config)); err != nil {\n\t\treturn fmt.Errorf(\"sync config table failed: %w\", err)\n\t}\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 36, Key: \"rank.question.add\", Value: `1`},\n\t\t{ID: 37, Key: \"rank.question.edit\", Value: `200`},\n\t\t{ID: 38, Key: \"rank.question.delete\", Value: `-1`},\n\t\t{ID: 39, Key: \"rank.question.vote_up\", Value: `15`},\n\t\t{ID: 40, Key: \"rank.question.vote_down\", Value: `125`},\n\t\t{ID: 41, Key: \"rank.answer.add\", Value: `1`},\n\t\t{ID: 42, Key: \"rank.answer.edit\", Value: `200`},\n\t\t{ID: 43, Key: \"rank.answer.delete\", Value: `-1`},\n\t\t{ID: 44, Key: \"rank.answer.accept\", Value: `-1`},\n\t\t{ID: 45, Key: \"rank.answer.vote_up\", Value: `15`},\n\t\t{ID: 46, Key: \"rank.answer.vote_down\", Value: `125`},\n\t\t{ID: 47, Key: \"rank.comment.add\", Value: `1`},\n\t\t{ID: 48, Key: \"rank.comment.edit\", Value: `-1`},\n\t\t{ID: 49, Key: \"rank.comment.delete\", Value: `-1`},\n\t\t{ID: 50, Key: \"rank.report.add\", Value: `1`},\n\t\t{ID: 51, Key: \"rank.tag.add\", Value: `1500`},\n\t\t{ID: 52, Key: \"rank.tag.edit\", Value: `100`},\n\t\t{ID: 53, Key: \"rank.tag.delete\", Value: `-1`},\n\t\t{ID: 54, Key: \"rank.tag.synonym\", Value: `20000`},\n\t\t{ID: 55, Key: \"rank.link.url_limit\", Value: `10`},\n\t\t{ID: 56, Key: \"rank.vote.detail\", Value: `0`},\n\n\t\t{ID: 87, Key: \"question.asked\", Value: `0`},\n\t\t{ID: 88, Key: \"question.closed\", Value: `0`},\n\t\t{ID: 89, Key: \"question.reopened\", Value: `0`},\n\t\t{ID: 90, Key: \"question.answered\", Value: `0`},\n\t\t{ID: 91, Key: \"question.commented\", Value: `0`},\n\t\t{ID: 92, Key: \"question.accept\", Value: `0`},\n\t\t{ID: 93, Key: \"question.edited\", Value: `0`},\n\t\t{ID: 94, Key: \"question.rollback\", Value: `0`},\n\t\t{ID: 95, Key: \"question.deleted\", Value: `0`},\n\t\t{ID: 96, Key: \"question.undeleted\", Value: `0`},\n\t\t{ID: 97, Key: \"answer.answered\", Value: `0`},\n\t\t{ID: 98, Key: \"answer.commented\", Value: `0`},\n\t\t{ID: 99, Key: \"answer.edited\", Value: `0`},\n\t\t{ID: 100, Key: \"answer.rollback\", Value: `0`},\n\t\t{ID: 101, Key: \"answer.undeleted\", Value: `0`},\n\t\t{ID: 102, Key: \"tag.created\", Value: `0`},\n\t\t{ID: 103, Key: \"tag.edited\", Value: `0`},\n\t\t{ID: 104, Key: \"tag.rollback\", Value: `0`},\n\t\t{ID: 105, Key: \"tag.deleted\", Value: `0`},\n\t\t{ID: 106, Key: \"tag.undeleted\", Value: `0`},\n\n\t\t{ID: 107, Key: \"rank.comment.vote_up\", Value: `1`},\n\t\t{ID: 108, Key: \"rank.comment.vote_down\", Value: `1`},\n\t\t{ID: 109, Key: \"rank.question.edit_without_review\", Value: `2000`},\n\t\t{ID: 110, Key: \"rank.answer.edit_without_review\", Value: `2000`},\n\t\t{ID: 111, Key: \"rank.tag.edit_without_review\", Value: `20000`},\n\t\t{ID: 112, Key: \"rank.answer.audit\", Value: `2000`},\n\t\t{ID: 113, Key: \"rank.question.audit\", Value: `2000`},\n\t\t{ID: 114, Key: \"rank.tag.audit\", Value: `20000`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\n\ttype Revision struct {\n\t\tID           string `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tObjectID     string `xorm:\"not null default 0 BIGINT(20) INDEX object_id\"`\n\t\tReviewUserID int64  `xorm:\"not null default 0 BIGINT(20) review_user_id\"`\n\t}\n\ttype Activity struct {\n\t\tID               string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tCancelledAt      time.Time `xorm:\"TIMESTAMP cancelled_at\"`\n\t\tUserID           string    `xorm:\"not null index BIGINT(20) user_id\"`\n\t\tTriggerUserID    int64     `xorm:\"not null default 0 index BIGINT(20) trigger_user_id\"`\n\t\tObjectID         string    `xorm:\"not null default 0 index BIGINT(20) object_id\"`\n\t\tRevisionID       int64     `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n\t\tOriginalObjectID string    `xorm:\"not null default 0 BIGINT(20) original_object_id\"`\n\t}\n\ttype Tag struct {\n\t\tID       string `xorm:\"not null pk comment('tag_id') BIGINT(20) id\"`\n\t\tSlugName string `xorm:\"not null default '' unique VARCHAR(35) slug_name\"`\n\t\tUserID   string `xorm:\"not null default 0 BIGINT(20) user_id\"`\n\t}\n\ttype Question struct {\n\t\tID             string    `xorm:\"not null pk BIGINT(20) id\"`\n\t\tUserID         string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\t\tUpdatedAt      time.Time `xorm:\"updated_at TIMESTAMP\"`\n\t\tLastEditUserID string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\t\tPostUpdateTime time.Time `xorm:\"post_update_time TIMESTAMP\"`\n\t}\n\ttype Answer struct {\n\t\tID             string    `xorm:\"not null pk autoincr BIGINT(20) id\"`\n\t\tUserID         string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\t\tUpdatedAt      time.Time `xorm:\"updated_at TIMESTAMP\"`\n\t\tLastEditUserID string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\t}\n\n\terr = x.Context(ctx).Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sync table failed %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v30.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\nfunc updateAdminMenuSettings(ctx context.Context, x *xorm.Engine) (err error) {\n\terr = splitWriteMenu(ctx, x)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = splitInterfaceMenu(ctx, x)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = splitLegalMenu(ctx, x)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\n// splitWriteMenu splits the site write settings into advanced, questions, and tags settings\nfunc splitWriteMenu(ctx context.Context, x *xorm.Engine) error {\n\tvar (\n\t\tsiteInfo          = &entity.SiteInfo{}\n\t\tsiteInfoAdvanced  = &entity.SiteInfo{}\n\t\tsiteInfoQuestions = &entity.SiteInfo{}\n\t\tsiteInfoTags      = &entity.SiteInfo{}\n\t)\n\texist, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeWrite}).Get(siteInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tsiteWrite := &schema.SiteWriteResp{}\n\tif err := json.Unmarshal([]byte(siteInfo.Content), siteWrite); err != nil {\n\t\treturn err\n\t}\n\t// site advanced settings\n\tsiteAdvanced := &schema.SiteAdvancedResp{\n\t\tMaxImageSize:                   siteWrite.MaxImageSize,\n\t\tMaxAttachmentSize:              siteWrite.MaxAttachmentSize,\n\t\tMaxImageMegapixel:              siteWrite.MaxImageMegapixel,\n\t\tAuthorizedImageExtensions:      siteWrite.AuthorizedImageExtensions,\n\t\tAuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions,\n\t}\n\t// site questions settings\n\tsiteQuestions := &schema.SiteQuestionsResp{\n\t\tMinimumTags:    siteWrite.MinimumTags,\n\t\tMinimumContent: siteWrite.MinimumContent,\n\t\tRestrictAnswer: siteWrite.RestrictAnswer,\n\t}\n\t// site tags settings\n\tsiteTags := &schema.SiteTagsResp{\n\t\tReservedTags:  siteWrite.ReservedTags,\n\t\tRecommendTags: siteWrite.RecommendTags,\n\t\tRequiredTag:   siteWrite.RequiredTag,\n\t}\n\n\t// save site settings\n\t// save advanced settings\n\texistsAdvanced, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeWrite}).Get(siteInfoAdvanced)\n\tif err != nil {\n\t\treturn err\n\t}\n\tadvancedContent, err := json.Marshal(siteAdvanced)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsAdvanced {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeAdvanced,\n\t\t\tContent: string(advancedContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save questions settings\n\texistsQuestions, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeQuestions}).Get(siteInfoQuestions)\n\tif err != nil {\n\t\treturn err\n\t}\n\tquestionsContent, err := json.Marshal(siteQuestions)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsQuestions {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeQuestions,\n\t\t\tContent: string(questionsContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save tags settings\n\texistsTags, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeTags}).Get(siteInfoTags)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttagsContent, err := json.Marshal(siteTags)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsTags {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeTags,\n\t\t\tContent: string(tagsContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// splitInterfaceMenu splits the site interface settings into interface and user settings\nfunc splitInterfaceMenu(ctx context.Context, x *xorm.Engine) error {\n\tvar (\n\t\tsiteInfo          = &entity.SiteInfo{}\n\t\tsiteInfoInterface = &entity.SiteInfo{}\n\t\tsiteInfoUsers     = &entity.SiteInfo{}\n\t)\n\ttype SiteInterface struct {\n\t\tLanguage        string `validate:\"required,gt=1,lte=128\" form:\"language\" json:\"language\"`\n\t\tTimeZone        string `validate:\"required,gt=1,lte=128\" form:\"time_zone\" json:\"time_zone\"`\n\t\tDefaultAvatar   string `validate:\"required,oneof=system gravatar\" json:\"default_avatar\"`\n\t\tGravatarBaseURL string `validate:\"omitempty\" json:\"gravatar_base_url\"`\n\t}\n\n\texist, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeInterface}).Get(siteInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\toldSiteInterface := &SiteInterface{}\n\tif err := json.Unmarshal([]byte(siteInfo.Content), oldSiteInterface); err != nil {\n\t\treturn err\n\t}\n\tsiteUser := &schema.SiteUsersSettingsResp{\n\t\tDefaultAvatar:   oldSiteInterface.DefaultAvatar,\n\t\tGravatarBaseURL: oldSiteInterface.GravatarBaseURL,\n\t}\n\tsiteInterface := &schema.SiteInterfaceResp{\n\t\tLanguage: oldSiteInterface.Language,\n\t\tTimeZone: oldSiteInterface.TimeZone,\n\t}\n\n\t// save settings\n\t// save user settings\n\texistsUsers, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeUsersSettings}).Get(siteInfoUsers)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserContent, err := json.Marshal(siteUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsUsers {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeUsersSettings,\n\t\t\tContent: string(userContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save interface settings\n\texistsInterface, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeInterfaceSettings}).Get(siteInfoInterface)\n\tif err != nil {\n\t\treturn err\n\t}\n\tinterfaceContent, err := json.Marshal(siteInterface)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsInterface {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeInterfaceSettings,\n\t\t\tContent: string(interfaceContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// splitLegalMenu splits the site legal settings into policies and security settings\nfunc splitLegalMenu(ctx context.Context, x *xorm.Engine) error {\n\tvar (\n\t\tsiteInfo         = &entity.SiteInfo{}\n\t\tsiteInfoPolices  = &entity.SiteInfo{}\n\t\tsiteInfoSecurity = &entity.SiteInfo{}\n\t\tsiteInfoLogin    = &entity.SiteInfo{}\n\t\tsiteInfoGeneral  = &entity.SiteInfo{}\n\t)\n\n\ttype SiteLogin struct {\n\t\tAllowNewRegistrations   bool     `json:\"allow_new_registrations\"`\n\t\tAllowEmailRegistrations bool     `json:\"allow_email_registrations\"`\n\t\tAllowPasswordLogin      bool     `json:\"allow_password_login\"`\n\t\tLoginRequired           bool     `json:\"login_required\"`\n\t\tAllowEmailDomains       []string `json:\"allow_email_domains\"`\n\t}\n\n\ttype SiteGeneral struct {\n\t\tName             string `validate:\"required,sanitizer,gt=1,lte=128\" form:\"name\" json:\"name\"`\n\t\tShortDescription string `validate:\"omitempty,sanitizer,gt=3,lte=255\" form:\"short_description\" json:\"short_description\"`\n\t\tDescription      string `validate:\"omitempty,sanitizer,gt=3,lte=2000\" form:\"description\" json:\"description\"`\n\t\tSiteUrl          string `validate:\"required,sanitizer,gt=1,lte=512,url\" form:\"site_url\" json:\"site_url\"`\n\t\tContactEmail     string `validate:\"required,sanitizer,gt=1,lte=512,email\" form:\"contact_email\" json:\"contact_email\"`\n\t\tCheckUpdate      bool   `validate:\"omitempty,sanitizer\" form:\"check_update\" json:\"check_update\"`\n\t}\n\n\t// find old site legal settings\n\texist, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeLegal}).Get(siteInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\toldSiteLegal := &schema.SiteLegalResp{}\n\tif err := json.Unmarshal([]byte(siteInfo.Content), oldSiteLegal); err != nil {\n\t\treturn err\n\t}\n\n\t// find old site login settings\n\texistsLogin, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeLogin}).Get(siteInfoLogin)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldSiteLogin := &SiteLogin{}\n\tif err := json.Unmarshal([]byte(siteInfoLogin.Content), oldSiteLogin); err != nil {\n\t\treturn err\n\t}\n\n\t// find old site general settings\n\texistGeneral, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeGeneral}).Get(siteInfoGeneral)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldSiteGeneral := &SiteGeneral{}\n\tif err := json.Unmarshal([]byte(siteInfoGeneral.Content), oldSiteGeneral); err != nil {\n\t\treturn err\n\t}\n\n\tsitePolicies := &schema.SitePoliciesResp{\n\t\tTermsOfServiceOriginalText: oldSiteLegal.TermsOfServiceOriginalText,\n\t\tTermsOfServiceParsedText:   oldSiteLegal.TermsOfServiceParsedText,\n\t\tPrivacyPolicyOriginalText:  oldSiteLegal.PrivacyPolicyOriginalText,\n\t\tPrivacyPolicyParsedText:    oldSiteLegal.PrivacyPolicyParsedText,\n\t}\n\tsiteLogin := &schema.SiteLoginResp{\n\t\tAllowNewRegistrations:   oldSiteLogin.AllowNewRegistrations,\n\t\tAllowEmailRegistrations: oldSiteLogin.AllowEmailRegistrations,\n\t\tAllowPasswordLogin:      oldSiteLogin.AllowPasswordLogin,\n\t\tAllowEmailDomains:       oldSiteLogin.AllowEmailDomains,\n\t}\n\tsiteGeneral := &schema.SiteGeneralReq{\n\t\tName:             oldSiteGeneral.Name,\n\t\tShortDescription: oldSiteGeneral.ShortDescription,\n\t\tDescription:      oldSiteGeneral.Description,\n\t\tSiteUrl:          oldSiteGeneral.SiteUrl,\n\t\tContactEmail:     oldSiteGeneral.ContactEmail,\n\t}\n\tsiteSecurity := &schema.SiteSecurityResp{\n\t\tLoginRequired:          oldSiteLogin.LoginRequired,\n\t\tExternalContentDisplay: oldSiteLegal.ExternalContentDisplay,\n\t\tCheckUpdate:            oldSiteGeneral.CheckUpdate,\n\t}\n\tif !existsLogin {\n\t\tsiteSecurity.LoginRequired = false\n\t}\n\tif !existGeneral {\n\t\tsiteSecurity.CheckUpdate = true\n\t}\n\n\t// save settings\n\t// save policies settings\n\texistsPolicies, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypePolicies}).Get(siteInfoPolices)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpoliciesContent, err := json.Marshal(sitePolicies)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsPolicies {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypePolicies,\n\t\t\tContent: string(policiesContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save security settings\n\texistsSecurity, err := x.Context(ctx).Where(builder.Eq{\"type\": constant.SiteTypeSecurity}).Get(siteInfoSecurity)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsecurityContent, err := json.Marshal(siteSecurity)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !existsSecurity {\n\t\t_, err = x.Context(ctx).Insert(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeSecurity,\n\t\t\tContent: string(securityContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save login settings\n\tif existsLogin {\n\t\tloginContent, _ := json.Marshal(siteLogin)\n\t\t_, err = x.Context(ctx).ID(siteInfoLogin.ID).Update(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeLogin,\n\t\t\tContent: string(loginContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save general settings\n\tif existGeneral {\n\t\tgeneralContent, _ := json.Marshal(siteGeneral)\n\t\t_, err = x.Context(ctx).ID(siteInfoGeneral.ID).Update(&entity.SiteInfo{\n\t\t\tType:    constant.SiteTypeGeneral,\n\t\t\tContent: string(generalContent),\n\t\t\tStatus:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v31.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc aiFeat(ctx context.Context, x *xorm.Engine) error {\n\tif err := addAIConversationTables(ctx, x); err != nil {\n\t\treturn fmt.Errorf(\"add ai conversation tables failed: %w\", err)\n\t}\n\tif err := addAPIKey(ctx, x); err != nil {\n\t\treturn fmt.Errorf(\"add api key failed: %w\", err)\n\t}\n\tlog.Info(\"AI feature migration completed successfully\")\n\treturn nil\n}\n\nfunc addAIConversationTables(ctx context.Context, x *xorm.Engine) error {\n\tif err := x.Context(ctx).Sync(new(entity.AIConversation)); err != nil {\n\t\treturn fmt.Errorf(\"sync ai_conversation table failed: %w\", err)\n\t}\n\n\tif err := x.Context(ctx).Sync(new(entity.AIConversationRecord)); err != nil {\n\t\treturn fmt.Errorf(\"sync ai_conversation_record table failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc addAPIKey(ctx context.Context, x *xorm.Engine) error {\n\terr := x.Context(ctx).Sync(new(entity.APIKey))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 131, Key: \"ai_config.provider\", Value: `[{\"default_api_host\":\"https://api.openai.com\",\"display_name\":\"OpenAI\",\"name\":\"openai\"},{\"default_api_host\":\"https://generativelanguage.googleapis.com\",\"display_name\":\"Gemini\",\"name\":\"gemini\"},{\"default_api_host\":\"https://api.anthropic.com\",\"display_name\":\"Anthropic\",\"name\":\"anthropic\"}]`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{Key: c.Key})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\n\taiSiteInfo := &entity.SiteInfo{\n\t\tType: constant.SiteTypeAI,\n\t}\n\texist, err := x.Context(ctx).Get(aiSiteInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif exist {\n\t\tcontent := &schema.SiteAIReq{}\n\t\t_ = json.Unmarshal([]byte(aiSiteInfo.Content), content)\n\t\tcontent.PromptConfig = &schema.AIPromptConfig{\n\t\t\tZhCN: constant.DefaultAIPromptConfigZhCN,\n\t\t\tEnUS: constant.DefaultAIPromptConfigEnUS,\n\t\t}\n\t\tdata, _ := json.Marshal(content)\n\t\taiSiteInfo.Content = string(data)\n\t\t_, err = x.Context(ctx).ID(aiSiteInfo.ID).Cols(\"content\").Update(aiSiteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"update site info failed: %w\", err)\n\t\t}\n\t} else {\n\t\tcontent := &schema.SiteAIReq{\n\t\t\tPromptConfig: &schema.AIPromptConfig{\n\t\t\t\tZhCN: constant.DefaultAIPromptConfigZhCN,\n\t\t\t\tEnUS: constant.DefaultAIPromptConfigEnUS,\n\t\t\t},\n\t\t}\n\t\tdata, _ := json.Marshal(content)\n\t\taiSiteInfo.Content = string(data)\n\t\taiSiteInfo.Type = constant.SiteTypeAI\n\t\tif _, err = x.Context(ctx).Insert(aiSiteInfo); err != nil {\n\t\t\treturn fmt.Errorf(\"insert site info failed: %w\", err)\n\t\t}\n\t\tlog.Infof(\"insert site info %+v\", aiSiteInfo)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v4.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addRoleFeatures(ctx context.Context, x *xorm.Engine) error {\n\terr := x.Context(ctx).Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\troles := []*entity.Role{\n\t\t{ID: 1, Name: \"User\", Description: \"Default with no special access.\"},\n\t\t{ID: 2, Name: \"Admin\", Description: \"Have the full power to access the site.\"},\n\t\t{ID: 3, Name: \"Moderator\", Description: \"Has access to all posts except admin settings.\"},\n\t}\n\n\t// insert default roles\n\tfor _, role := range roles {\n\t\texist, err := x.Context(ctx).Get(&entity.Role{ID: role.ID, Name: role.Name})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tpowers := []*entity.Power{\n\t\t{ID: 1, Name: \"admin access\", PowerType: permission.AdminAccess, Description: \"admin access\"},\n\t\t{ID: 2, Name: \"question add\", PowerType: permission.QuestionAdd, Description: \"question add\"},\n\t\t{ID: 3, Name: \"question edit\", PowerType: permission.QuestionEdit, Description: \"question edit\"},\n\t\t{ID: 4, Name: \"question edit without review\", PowerType: permission.QuestionEditWithoutReview, Description: \"question edit without review\"},\n\t\t{ID: 5, Name: \"question delete\", PowerType: permission.QuestionDelete, Description: \"question delete\"},\n\t\t{ID: 6, Name: \"question close\", PowerType: permission.QuestionClose, Description: \"question close\"},\n\t\t{ID: 7, Name: \"question reopen\", PowerType: permission.QuestionReopen, Description: \"question reopen\"},\n\t\t{ID: 8, Name: \"question vote up\", PowerType: permission.QuestionVoteUp, Description: \"question vote up\"},\n\t\t{ID: 9, Name: \"question vote down\", PowerType: permission.QuestionVoteDown, Description: \"question vote down\"},\n\t\t{ID: 10, Name: \"answer add\", PowerType: permission.AnswerAdd, Description: \"answer add\"},\n\t\t{ID: 11, Name: \"answer edit\", PowerType: permission.AnswerEdit, Description: \"answer edit\"},\n\t\t{ID: 12, Name: \"answer edit without review\", PowerType: permission.AnswerEditWithoutReview, Description: \"answer edit without review\"},\n\t\t{ID: 13, Name: \"answer delete\", PowerType: permission.AnswerDelete, Description: \"answer delete\"},\n\t\t{ID: 14, Name: \"answer accept\", PowerType: permission.AnswerAccept, Description: \"answer accept\"},\n\t\t{ID: 15, Name: \"answer vote up\", PowerType: permission.AnswerVoteUp, Description: \"answer vote up\"},\n\t\t{ID: 16, Name: \"answer vote down\", PowerType: permission.AnswerVoteDown, Description: \"answer vote down\"},\n\t\t{ID: 17, Name: \"comment add\", PowerType: permission.CommentAdd, Description: \"comment add\"},\n\t\t{ID: 18, Name: \"comment edit\", PowerType: permission.CommentEdit, Description: \"comment edit\"},\n\t\t{ID: 19, Name: \"comment delete\", PowerType: permission.CommentDelete, Description: \"comment delete\"},\n\t\t{ID: 20, Name: \"comment vote up\", PowerType: permission.CommentVoteUp, Description: \"comment vote up\"},\n\t\t{ID: 21, Name: \"comment vote down\", PowerType: permission.CommentVoteDown, Description: \"comment vote down\"},\n\t\t{ID: 22, Name: \"report add\", PowerType: permission.ReportAdd, Description: \"report add\"},\n\t\t{ID: 23, Name: \"tag add\", PowerType: permission.TagAdd, Description: \"tag add\"},\n\t\t{ID: 24, Name: \"tag edit\", PowerType: permission.TagEdit, Description: \"tag edit\"},\n\t\t{ID: 25, Name: \"tag edit without review\", PowerType: permission.TagEditWithoutReview, Description: \"tag edit without review\"},\n\t\t{ID: 26, Name: \"tag edit slug name\", PowerType: permission.TagEditSlugName, Description: \"tag edit slug name\"},\n\t\t{ID: 27, Name: \"tag delete\", PowerType: permission.TagDelete, Description: \"tag delete\"},\n\t\t{ID: 28, Name: \"tag synonym\", PowerType: permission.TagSynonym, Description: \"tag synonym\"},\n\t\t{ID: 29, Name: \"link url limit\", PowerType: permission.LinkUrlLimit, Description: \"link url limit\"},\n\t\t{ID: 30, Name: \"vote detail\", PowerType: permission.VoteDetail, Description: \"vote detail\"},\n\t\t{ID: 31, Name: \"answer audit\", PowerType: permission.AnswerAudit, Description: \"answer audit\"},\n\t\t{ID: 32, Name: \"question audit\", PowerType: permission.QuestionAudit, Description: \"question audit\"},\n\t\t{ID: 33, Name: \"tag audit\", PowerType: permission.TagAudit, Description: \"tag audit\"},\n\t}\n\t// insert default powers\n\tfor _, power := range powers {\n\t\texist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\t_, err = x.Context(ctx).ID(power.ID).Update(power)\n\t\t} else {\n\t\t\t_, err = x.Context(ctx).Insert(power)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trolePowerRels := []*entity.RolePowerRel{\n\t\t{RoleID: 2, PowerType: permission.AdminAccess},\n\t\t{RoleID: 2, PowerType: permission.QuestionAdd},\n\t\t{RoleID: 2, PowerType: permission.QuestionEdit},\n\t\t{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.QuestionDelete},\n\t\t{RoleID: 2, PowerType: permission.QuestionClose},\n\t\t{RoleID: 2, PowerType: permission.QuestionReopen},\n\t\t{RoleID: 2, PowerType: permission.QuestionVoteUp},\n\t\t{RoleID: 2, PowerType: permission.QuestionVoteDown},\n\t\t{RoleID: 2, PowerType: permission.AnswerAdd},\n\t\t{RoleID: 2, PowerType: permission.AnswerEdit},\n\t\t{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.AnswerDelete},\n\t\t{RoleID: 2, PowerType: permission.AnswerAccept},\n\t\t{RoleID: 2, PowerType: permission.AnswerVoteUp},\n\t\t{RoleID: 2, PowerType: permission.AnswerVoteDown},\n\t\t{RoleID: 2, PowerType: permission.CommentAdd},\n\t\t{RoleID: 2, PowerType: permission.CommentEdit},\n\t\t{RoleID: 2, PowerType: permission.CommentDelete},\n\t\t{RoleID: 2, PowerType: permission.CommentVoteUp},\n\t\t{RoleID: 2, PowerType: permission.CommentVoteDown},\n\t\t{RoleID: 2, PowerType: permission.ReportAdd},\n\t\t{RoleID: 2, PowerType: permission.TagAdd},\n\t\t{RoleID: 2, PowerType: permission.TagEdit},\n\t\t{RoleID: 2, PowerType: permission.TagEditSlugName},\n\t\t{RoleID: 2, PowerType: permission.TagEditWithoutReview},\n\t\t{RoleID: 2, PowerType: permission.TagDelete},\n\t\t{RoleID: 2, PowerType: permission.TagSynonym},\n\t\t{RoleID: 2, PowerType: permission.LinkUrlLimit},\n\t\t{RoleID: 2, PowerType: permission.VoteDetail},\n\t\t{RoleID: 2, PowerType: permission.AnswerAudit},\n\t\t{RoleID: 2, PowerType: permission.QuestionAudit},\n\t\t{RoleID: 2, PowerType: permission.TagAudit},\n\t\t{RoleID: 2, PowerType: permission.TagUseReservedTag},\n\n\t\t{RoleID: 3, PowerType: permission.QuestionAdd},\n\t\t{RoleID: 3, PowerType: permission.QuestionEdit},\n\t\t{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.QuestionDelete},\n\t\t{RoleID: 3, PowerType: permission.QuestionClose},\n\t\t{RoleID: 3, PowerType: permission.QuestionReopen},\n\t\t{RoleID: 3, PowerType: permission.QuestionVoteUp},\n\t\t{RoleID: 3, PowerType: permission.QuestionVoteDown},\n\t\t{RoleID: 3, PowerType: permission.AnswerAdd},\n\t\t{RoleID: 3, PowerType: permission.AnswerEdit},\n\t\t{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.AnswerDelete},\n\t\t{RoleID: 3, PowerType: permission.AnswerAccept},\n\t\t{RoleID: 3, PowerType: permission.AnswerVoteUp},\n\t\t{RoleID: 3, PowerType: permission.AnswerVoteDown},\n\t\t{RoleID: 3, PowerType: permission.CommentAdd},\n\t\t{RoleID: 3, PowerType: permission.CommentEdit},\n\t\t{RoleID: 3, PowerType: permission.CommentDelete},\n\t\t{RoleID: 3, PowerType: permission.CommentVoteUp},\n\t\t{RoleID: 3, PowerType: permission.CommentVoteDown},\n\t\t{RoleID: 3, PowerType: permission.ReportAdd},\n\t\t{RoleID: 3, PowerType: permission.TagAdd},\n\t\t{RoleID: 3, PowerType: permission.TagEdit},\n\t\t{RoleID: 3, PowerType: permission.TagEditSlugName},\n\t\t{RoleID: 3, PowerType: permission.TagEditWithoutReview},\n\t\t{RoleID: 3, PowerType: permission.TagDelete},\n\t\t{RoleID: 3, PowerType: permission.TagSynonym},\n\t\t{RoleID: 3, PowerType: permission.LinkUrlLimit},\n\t\t{RoleID: 3, PowerType: permission.VoteDetail},\n\t\t{RoleID: 3, PowerType: permission.AnswerAudit},\n\t\t{RoleID: 3, PowerType: permission.QuestionAudit},\n\t\t{RoleID: 3, PowerType: permission.TagAudit},\n\t\t{RoleID: 3, PowerType: permission.TagUseReservedTag},\n\t}\n\n\t// insert default powers\n\tfor _, rel := range rolePowerRels {\n\t\texist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(rel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tadminUserRoleRel := &entity.UserRoleRel{\n\t\tUserID: \"1\",\n\t\tRoleID: 2,\n\t}\n\n\texist, err := x.Context(ctx).Get(adminUserRoleRel)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\t_, err = x.Context(ctx).Insert(adminUserRoleRel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 115, Key: \"rank.question.close\", Value: `-1`},\n\t\t{ID: 116, Key: \"rank.question.reopen\", Value: `-1`},\n\t\t{ID: 117, Key: \"rank.tag.use_reserved_tag\", Value: `-1`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v5.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addThemeAndPrivateMode(ctx context.Context, x *xorm.Engine) error {\n\tloginConfig := map[string]bool{\n\t\t\"allow_new_registrations\": true,\n\t\t\"login_required\":          false,\n\t}\n\tloginConfigDataBytes, _ := json.Marshal(loginConfig)\n\tsiteInfo := &entity.SiteInfo{\n\t\tType:    \"login\",\n\t\tContent: string(loginConfigDataBytes),\n\t\tStatus:  1,\n\t}\n\texist, err := x.Context(ctx).Get(&entity.SiteInfo{Type: siteInfo.Type})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\t_, err = x.Context(ctx).Insert(siteInfo)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"insert site info failed: %w\", err)\n\t\t}\n\t}\n\n\tthemeConfig := fmt.Sprintf(`{\"theme\":\"default\",\"theme_config\":{\"default\":{\"navbar_style\":\"#0033ff\",\"primary_color\":\"#0033ff\"}},\"layout\":\"%s\"}`, constant.ThemeLayoutFullWidth)\n\tthemeSiteInfo := &entity.SiteInfo{\n\t\tType:    \"theme\",\n\t\tContent: themeConfig,\n\t\tStatus:  1,\n\t}\n\texist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: themeSiteInfo.Type})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t}\n\tif !exist {\n\t\t_, err = x.Context(ctx).Insert(themeSiteInfo)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/migrations/v6.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addNewAnswerNotification(ctx context.Context, x *xorm.Engine) error {\n\tcond := &entity.Config{Key: \"email.config\"}\n\texists, err := x.Context(ctx).Get(cond)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get email config failed: %w\", err)\n\t}\n\tif !exists {\n\t\t// This should be impossible except that the config was deleted manually by user.\n\t\t_, err = x.Context(ctx).Insert(&entity.Config{\n\t\t\tKey:   \"email.config\",\n\t\t\tValue: `{\"from_name\":\"\",\"from_email\":\"\",\"smtp_host\":\"\",\"smtp_port\":465,\"smtp_password\":\"\",\"smtp_username\":\"\",\"smtp_authentication\":true,\"encryption\":\"\",\"register_title\":\"[{{.SiteName}}] Confirm your new account\",\"register_body\":\"Welcome to {{.SiteName}}<br><br>\\n\\nClick the following link to confirm and activate your new account:<br>\\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\\n\\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\\n\",\"pass_reset_title\":\"[{{.SiteName }}] Password reset\",\"pass_reset_body\":\"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\\n\\nIf it was not you, you can safely ignore this email.<br><br>\\n\\nClick the following link to choose a new password:<br>\\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\\n\",\"change_title\":\"[{{.SiteName}}] Confirm your new email address\",\"change_body\":\"Confirm your new email address for {{.SiteName}}  by clicking on the following link:<br><br>\\n\\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\\n\\nIf you did not request this change, please ignore this email.\\n\",\"test_title\":\"[{{.SiteName}}] Test Email\",\"test_body\":\"This is a test email.\",\"new_answer_title\":\"[{{.SiteName}}] {{.DisplayName}} answered your question\",\"new_answer_body\":\"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\",\"new_comment_title\":\"[{{.SiteName}}] {{.DisplayName}} commented on your post\",\"new_comment_body\":\"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"}`,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"add email config failed: %v\", err)\n\t\t}\n\t}\n\n\tm := make(map[string]any)\n\t_ = json.Unmarshal([]byte(cond.Value), &m)\n\tm[\"new_answer_title\"] = \"[{{.SiteName}}] {{.DisplayName}} answered your question\"\n\tm[\"new_answer_body\"] = \"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.AnswerSummary}}</blockquote><br>\\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n\tm[\"new_comment_title\"] = \"[{{.SiteName}}] {{.DisplayName}} commented on your post\"\n\tm[\"new_comment_body\"] = \"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\\n\\n<small>{{.DisplayName}}:</small><br>\\n<blockquote>{{.CommentSummary}}</blockquote><br>\\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\\n\\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>\"\n\n\tval, _ := json.Marshal(m)\n\t_, err = x.Context(ctx).ID(cond.ID).Update(&entity.Config{Value: string(val)})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"update email config failed: %v\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v7.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addPlugin(ctx context.Context, x *xorm.Engine) error {\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 118, Key: \"plugin.status\", Value: `{}`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\n\treturn x.Context(ctx).Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))\n}\n"
  },
  {
    "path": "internal/migrations/v8.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc addRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error {\n\tpowers := []*entity.Power{\n\t\t{ID: 34, Name: \"question pin\", PowerType: permission.QuestionPin, Description: \"top the question\"},\n\t\t{ID: 35, Name: \"question hide\", PowerType: permission.QuestionHide, Description: \"hide  the question\"},\n\t\t{ID: 36, Name: \"question unpin\", PowerType: permission.QuestionUnPin, Description: \"untop the question\"},\n\t\t{ID: 37, Name: \"question show\", PowerType: permission.QuestionShow, Description: \"show the question\"},\n\t}\n\t// insert default powers\n\tfor _, power := range powers {\n\t\texist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\t_, err = x.Context(ctx).ID(power.ID).Update(power)\n\t\t} else {\n\t\t\t_, err = x.Context(ctx).Insert(power)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trolePowerRels := []*entity.RolePowerRel{\n\n\t\t{RoleID: 2, PowerType: permission.QuestionPin},\n\t\t{RoleID: 2, PowerType: permission.QuestionHide},\n\t\t{RoleID: 2, PowerType: permission.QuestionUnPin},\n\t\t{RoleID: 2, PowerType: permission.QuestionShow},\n\n\t\t{RoleID: 3, PowerType: permission.QuestionPin},\n\t\t{RoleID: 3, PowerType: permission.QuestionHide},\n\t\t{RoleID: 3, PowerType: permission.QuestionUnPin},\n\t\t{RoleID: 3, PowerType: permission.QuestionShow},\n\t}\n\n\t// insert default powers\n\tfor _, rel := range rolePowerRels {\n\t\texist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\t_, err = x.Context(ctx).Insert(rel)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdefaultConfigTable := []*entity.Config{\n\t\t{ID: 119, Key: \"question.pin\", Value: `0`},\n\t\t{ID: 120, Key: \"question.unpin\", Value: `0`},\n\t\t{ID: 121, Key: \"question.show\", Value: `0`},\n\t\t{ID: 122, Key: \"question.hide\", Value: `0`},\n\t\t{ID: 123, Key: \"rank.question.pin\", Value: `-1`},\n\t\t{ID: 124, Key: \"rank.question.unpin\", Value: `-1`},\n\t\t{ID: 125, Key: \"rank.question.show\", Value: `-1`},\n\t\t{ID: 126, Key: \"rank.question.hide\", Value: `-1`},\n\t}\n\tfor _, c := range defaultConfigTable {\n\t\texist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get config failed: %w\", err)\n\t\t}\n\t\tif exist {\n\t\t\tif _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil {\n\t\t\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\t\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {\n\t\t\tlog.Errorf(\"insert %+v config failed: %s\", c, err)\n\t\t\treturn fmt.Errorf(\"add config failed: %w\", err)\n\t\t}\n\t}\n\n\ttype Question struct {\n\t\tID               string    `xorm:\"not null pk BIGINT(20) id\"`\n\t\tCreatedAt        time.Time `xorm:\"not null default CURRENT_TIMESTAMP TIMESTAMP created_at\"`\n\t\tUpdatedAt        time.Time `xorm:\"updated_at TIMESTAMP\"`\n\t\tUserID           string    `xorm:\"not null default 0 BIGINT(20) INDEX user_id\"`\n\t\tLastEditUserID   string    `xorm:\"not null default 0 BIGINT(20) last_edit_user_id\"`\n\t\tTitle            string    `xorm:\"not null default '' VARCHAR(150) title\"`\n\t\tOriginalText     string    `xorm:\"not null MEDIUMTEXT original_text\"`\n\t\tParsedText       string    `xorm:\"not null MEDIUMTEXT parsed_text\"`\n\t\tStatus           int       `xorm:\"not null default 1 INT(11) status\"`\n\t\tPin              int       `xorm:\"not null default 1 INT(11) pin\"`\n\t\tShow             int       `xorm:\"not null default 1 INT(11) show\"`\n\t\tViewCount        int       `xorm:\"not null default 0 INT(11) view_count\"`\n\t\tUniqueViewCount  int       `xorm:\"not null default 0 INT(11) unique_view_count\"`\n\t\tVoteCount        int       `xorm:\"not null default 0 INT(11) vote_count\"`\n\t\tAnswerCount      int       `xorm:\"not null default 0 INT(11) answer_count\"`\n\t\tCollectionCount  int       `xorm:\"not null default 0 INT(11) collection_count\"`\n\t\tFollowCount      int       `xorm:\"not null default 0 INT(11) follow_count\"`\n\t\tAcceptedAnswerID string    `xorm:\"not null default 0 BIGINT(20) accepted_answer_id\"`\n\t\tLastAnswerID     string    `xorm:\"not null default 0 BIGINT(20) last_answer_id\"`\n\t\tPostUpdateTime   time.Time `xorm:\"post_update_time TIMESTAMP\"`\n\t\tRevisionID       string    `xorm:\"not null default 0 BIGINT(20) revision_id\"`\n\t}\n\terr := x.Context(ctx).Sync(new(Question))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/migrations/v9.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage migrations\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nfunc updateAcceptAnswerRank(ctx context.Context, x *xorm.Engine) error {\n\tc := &entity.Config{ID: 44, Key: \"rank.answer.accept\", Value: `-1`}\n\tif _, err := x.Context(ctx).Update(c, &entity.Config{ID: 44, Key: \"rank.answer.accept\"}); err != nil {\n\t\tlog.Errorf(\"update %+v config failed: %s\", c, err)\n\t\treturn fmt.Errorf(\"update config failed: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/activity/activity_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_type\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// activityRepo activity repository\ntype activityRepo struct {\n\tdata          *data.Data\n\tconfigService *config.ConfigService\n}\n\n// NewActivityRepo new repository\nfunc NewActivityRepo(\n\tdata *data.Data,\n\tconfigService *config.ConfigService,\n) activity.ActivityRepo {\n\treturn &activityRepo{\n\t\tdata:          data,\n\t\tconfigService: configService,\n\t}\n}\n\nfunc (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (\n\tactivityList []*entity.Activity, err error) {\n\tactivityList = make([]*entity.Activity, 0)\n\tsession := ar.data.DB.Context(ctx).Desc(\"id\")\n\n\tif !showVote {\n\t\tactivityTypeNotShown := ar.getAllActivityType(ctx)\n\t\tsession.NotIn(\"activity_type\", activityTypeNotShown)\n\t}\n\terr = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn activityList, nil\n}\n\nfunc (ar *activityRepo) getAllActivityType(ctx context.Context) (activityTypes []int) {\n\tvar activityTypeNotShown []int\n\tfor _, key := range activity_type.VoteActivityTypeList {\n\t\tid, err := ar.configService.GetIDByKey(ctx, key)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get config id by key [%s] error: %v\", key, err)\n\t\t} else {\n\t\t\tactivityTypeNotShown = append(activityTypeNotShown, id)\n\t\t}\n\t}\n\treturn activityTypeNotShown\n}\n"
  },
  {
    "path": "internal/repo/activity/answer_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// AnswerActivityRepo answer accepted\ntype AnswerActivityRepo struct {\n\tdata                     *data.Data\n\tactivityRepo             activity_common.ActivityRepo\n\tuserRankRepo             rank.UserRankRepo\n\tnotificationQueueService noticequeue.Service\n}\n\n// NewAnswerActivityRepo new repository\nfunc NewAnswerActivityRepo(\n\tdata *data.Data,\n\tactivityRepo activity_common.ActivityRepo,\n\tuserRankRepo rank.UserRankRepo,\n\tnotificationQueueService noticequeue.Service,\n) activity.AnswerActivityRepo {\n\treturn &AnswerActivityRepo{\n\t\tdata:                     data,\n\t\tactivityRepo:             activityRepo,\n\t\tuserRankRepo:             userRankRepo,\n\t\tnotificationQueueService: notificationQueueService,\n\t}\n}\n\nfunc (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (\n\terr error) {\n\t// save activity\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuserInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = ar.saveActivitiesAvailable(session, op)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = ar.changeUserRank(ctx, session, op, userInfoMapping)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// notification\n\tar.sendAcceptAnswerNotification(ctx, op)\n\treturn nil\n}\n\nfunc (ar *AnswerActivityRepo) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (\n\terr error) {\n\t// pre check\n\tactivities, err := ar.getExistActivity(ctx, op)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar userIDs []string\n\tfor _, act := range activities {\n\t\tif act.Cancelled == entity.ActivityCancelled {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, act.UserID)\n\t}\n\tif len(userIDs) == 0 {\n\t\treturn nil\n\t}\n\n\t// save activity\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuserInfoMapping, err := ar.acquireUserInfo(session, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = ar.cancelActivities(session, activities)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = ar.rollbackUserRank(ctx, session, activities, userInfoMapping)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// notification\n\tar.sendCancelAcceptAnswerNotification(ctx, op)\n\treturn nil\n}\n\nfunc (ar *AnswerActivityRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {\n\tus := make([]*entity.User, 0)\n\terr := session.In(\"id\", userIDs).ForUpdate().Find(&us)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn nil, err\n\t}\n\n\tusers := make(map[string]*entity.User, 0)\n\tfor _, u := range us {\n\t\tusers[u.ID] = u\n\t}\n\treturn users, nil\n}\n\n// saveActivitiesAvailable save activities\n// If activity not exist it will be created or else will be updated\n// If this activity is already exist, set activity rank to 0\n// So after this function, the activity rank will be correct for update user rank\nfunc (ar *AnswerActivityRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.AcceptAnswerOperationInfo) (\n\terr error) {\n\tfor _, act := range op.Activities {\n\t\texistsActivity := &entity.Activity{}\n\t\texist, err := session.\n\t\t\tWhere(builder.Eq{\"object_id\": op.AnswerObjectID}).\n\t\t\tAnd(builder.Eq{\"user_id\": act.ActivityUserID}).\n\t\t\tAnd(builder.Eq{\"trigger_user_id\": act.TriggerUserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": act.ActivityType}).\n\t\t\tGet(existsActivity)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist && existsActivity.Cancelled == entity.ActivityAvailable {\n\t\t\tact.Rank = 0\n\t\t\tcontinue\n\t\t}\n\t\tif exist {\n\t\t\tbean := &entity.Activity{\n\t\t\t\tCancelled: entity.ActivityAvailable,\n\t\t\t\tRank:      act.Rank,\n\t\t\t\tHasRank:   act.HasRank(),\n\t\t\t}\n\t\t\tsession.Where(\"id = ?\", existsActivity.ID)\n\t\t\tif _, err = session.Cols(\"`cancelled`\", \"`rank`\", \"`has_rank`\").Update(bean); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tinsertActivity := entity.Activity{\n\t\t\t\tObjectID:         op.AnswerObjectID,\n\t\t\t\tOriginalObjectID: act.OriginalObjectID,\n\t\t\t\tUserID:           act.ActivityUserID,\n\t\t\t\tTriggerUserID:    converter.StringToInt64(act.TriggerUserID),\n\t\t\t\tActivityType:     act.ActivityType,\n\t\t\t\tRank:             act.Rank,\n\t\t\t\tHasRank:          act.HasRank(),\n\t\t\t\tCancelled:        entity.ActivityAvailable,\n\t\t\t}\n\t\t\t_, err = session.Insert(&insertActivity)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// cancelActivities cancel activities\n// If this activity is already cancelled, set activity rank to 0\n// So after this function, the activity rank will be correct for update user rank\nfunc (ar *AnswerActivityRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {\n\tfor _, act := range activities {\n\t\tt := &entity.Activity{}\n\t\texist, err := session.ID(act.ID).Get(t)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\tlog.Error(fmt.Errorf(\"%s activity not exist\", act.ID))\n\t\t\treturn fmt.Errorf(\"%s activity not exist\", act.ID)\n\t\t}\n\t\t//  If this activity is already cancelled, set activity rank to 0\n\t\tif t.Cancelled == entity.ActivityCancelled {\n\t\t\tact.Rank = 0\n\t\t}\n\t\tif _, err = session.ID(act.ID).Cols(\"cancelled\", \"cancelled_at\").\n\t\t\tUpdate(&entity.Activity{\n\t\t\t\tCancelled:   entity.ActivityCancelled,\n\t\t\t\tCancelledAt: time.Now(),\n\t\t\t}); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ar *AnswerActivityRepo) changeUserRank(ctx context.Context, session *xorm.Session,\n\top *schema.AcceptAnswerOperationInfo,\n\tuserInfoMapping map[string]*entity.User) (err error) {\n\tfor _, act := range op.Activities {\n\t\tif act.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tuser := userInfoMapping[act.ActivityUserID]\n\t\tif user == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err = ar.userRankRepo.ChangeUserRank(ctx, session,\n\t\t\tact.ActivityUserID, user.Rank, act.Rank); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,\n\tactivities []*entity.Activity,\n\tuserInfoMapping map[string]*entity.User) (err error) {\n\tfor _, act := range activities {\n\t\tif act.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tuser := userInfoMapping[act.UserID]\n\t\tif user == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err = ar.userRankRepo.ChangeUserRank(ctx, session,\n\t\t\tact.UserID, user.Rank, -act.Rank); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) {\n\tvar activities []*entity.Activity\n\tfor _, action := range op.Activities {\n\t\tvar t []*entity.Activity\n\t\terr := ar.data.DB.Context(ctx).\n\t\t\tWhere(builder.Eq{\"user_id\": action.ActivityUserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": action.ActivityType}).\n\t\t\tAnd(builder.Eq{\"object_id\": op.AnswerObjectID}).\n\t\t\tFind(&t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif len(t) > 0 {\n\t\t\tactivities = append(activities, t...)\n\t\t}\n\t}\n\treturn activities, nil\n}\n\nfunc (ar *AnswerActivityRepo) sendAcceptAnswerNotification(\n\tctx context.Context, op *schema.AcceptAnswerOperationInfo) {\n\tfor _, act := range op.Activities {\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tType:           schema.NotificationTypeAchievement,\n\t\t\tObjectID:       op.AnswerObjectID,\n\t\t\tReceiverUserID: act.ActivityUserID,\n\t\t\tTriggerUserID:  act.TriggerUserID,\n\t\t}\n\t\tmsg.ObjectType = constant.AnswerObjectType\n\t\tif msg.TriggerUserID != msg.ReceiverUserID {\n\t\t\tar.notificationQueueService.Send(ctx, msg)\n\t\t}\n\t}\n\n\tfor _, act := range op.Activities {\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tReceiverUserID: act.ActivityUserID,\n\t\t\tType:           schema.NotificationTypeInbox,\n\t\t\tObjectID:       op.AnswerObjectID,\n\t\t\tTriggerUserID:  op.TriggerUserID,\n\t\t}\n\t\tif act.ActivityUserID != op.QuestionUserID {\n\t\t\tmsg.ObjectType = constant.AnswerObjectType\n\t\t\tmsg.NotificationAction = constant.NotificationAcceptAnswer\n\t\t\tar.notificationQueueService.Send(ctx, msg)\n\t\t}\n\t}\n}\n\nfunc (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification(\n\tctx context.Context, op *schema.AcceptAnswerOperationInfo) {\n\tfor _, act := range op.Activities {\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tTriggerUserID:  act.TriggerUserID,\n\t\t\tReceiverUserID: act.ActivityUserID,\n\t\t\tType:           schema.NotificationTypeAchievement,\n\t\t\tObjectID:       op.AnswerObjectID,\n\t\t}\n\t\tif act.ActivityUserID == op.QuestionObjectID {\n\t\t\tmsg.ObjectType = constant.QuestionObjectType\n\t\t} else {\n\t\t\tmsg.ObjectType = constant.AnswerObjectType\n\t\t}\n\t\tif msg.TriggerUserID != msg.ReceiverUserID {\n\t\t\tar.notificationQueueService.Send(ctx, msg)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/repo/activity/follow_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/follow\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// FollowRepo activity repository\ntype FollowRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n\tactivityRepo activity_common.ActivityRepo\n}\n\n// NewFollowRepo new repository\nfunc NewFollowRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n\tactivityRepo activity_common.ActivityRepo,\n) follow.FollowRepo {\n\treturn &FollowRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t\tactivityRepo: activityRepo,\n\t}\n}\n\nfunc (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error {\n\tobjectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, \"follow\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\tvar (\n\t\t\texistsActivity entity.Activity\n\t\t\thas            bool\n\t\t)\n\t\tresult = nil\n\n\t\thas, err = session.Where(builder.Eq{\"activity_type\": activityType}).\n\t\t\tAnd(builder.Eq{\"user_id\": userID}).\n\t\t\tAnd(builder.Eq{\"object_id\": objectID}).\n\t\t\tGet(&existsActivity)\n\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif has && existsActivity.Cancelled == entity.ActivityAvailable {\n\t\t\treturn\n\t\t}\n\n\t\tif has {\n\t\t\t_, err = session.Where(builder.Eq{\"id\": existsActivity.ID}).\n\t\t\t\tCols(`cancelled`).\n\t\t\t\tUpdate(&entity.Activity{\n\t\t\t\t\tCancelled: entity.ActivityAvailable,\n\t\t\t\t})\n\t\t} else {\n\t\t\t// update existing activity with new user id and u object id\n\t\t\t_, err = session.Insert(&entity.Activity{\n\t\t\t\tUserID:           userID,\n\t\t\t\tObjectID:         objectID,\n\t\t\t\tOriginalObjectID: objectID,\n\t\t\t\tActivityType:     activityType,\n\t\t\t\tCancelled:        entity.ActivityAvailable,\n\t\t\t\tRank:             0,\n\t\t\t\tHasRank:          0,\n\t\t\t})\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\t// start update followers when everything is fine\n\t\terr = ar.updateFollows(ctx, session, objectID, 1)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\n\t\treturn\n\t})\n\n\treturn err\n}\n\nfunc (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error {\n\tobjectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, \"follow\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\tvar (\n\t\t\texistsActivity entity.Activity\n\t\t\thas            bool\n\t\t)\n\t\tresult = nil\n\n\t\thas, err = session.Where(builder.Eq{\"activity_type\": activityType}).\n\t\t\tAnd(builder.Eq{\"user_id\": userID}).\n\t\t\tAnd(builder.Eq{\"object_id\": objectID}).\n\t\t\tGet(&existsActivity)\n\n\t\tif err != nil || !has {\n\t\t\treturn\n\t\t}\n\n\t\tif has && existsActivity.Cancelled == entity.ActivityCancelled {\n\t\t\treturn\n\t\t}\n\t\tif _, err = session.Where(\"id = ?\", existsActivity.ID).\n\t\t\tCols(\"cancelled\").\n\t\t\tUpdate(&entity.Activity{\n\t\t\t\tCancelled:   entity.ActivityCancelled,\n\t\t\t\tCancelledAt: time.Now(),\n\t\t\t}); err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = ar.updateFollows(ctx, session, objectID, -1)\n\t\treturn\n\t})\n\treturn err\n}\n\nfunc (ar *FollowRepo) updateFollows(_ context.Context, session *xorm.Session, objectID string, follows int) error {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch objectType {\n\tcase \"question\":\n\t\t_, err = session.Where(\"id = ?\", objectID).Incr(\"follow_count\", follows).Update(&entity.Question{})\n\tcase \"user\":\n\t\t_, err = session.Where(\"id = ?\", objectID).Incr(\"follow_count\", follows).Update(&entity.User{})\n\tcase \"tag\":\n\t\t_, err = session.Where(\"id = ?\", objectID).Incr(\"follow_count\", follows).Update(&entity.Tag{})\n\tdefault:\n\t\terr = errors.InternalServer(reason.DisallowFollow).WithMsg(\"this object can't be followed\")\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/repo/activity/review_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// ReviewActivityRepo answer accepted\ntype ReviewActivityRepo struct {\n\tdata          *data.Data\n\tactivityRepo  activity_common.ActivityRepo\n\tuserRankRepo  rank.UserRankRepo\n\tconfigService *config.ConfigService\n}\n\nconst (\n\tEditAccepted = \"edit.accepted\"\n)\n\n// NewReviewActivityRepo new repository\nfunc NewReviewActivityRepo(\n\tdata *data.Data,\n\tactivityRepo activity_common.ActivityRepo,\n\tuserRankRepo rank.UserRankRepo,\n\tconfigService *config.ConfigService,\n) activity.ReviewActivityRepo {\n\treturn &ReviewActivityRepo{\n\t\tdata:          data,\n\t\tactivityRepo:  activityRepo,\n\t\tuserRankRepo:  userRankRepo,\n\t\tconfigService: configService,\n\t}\n}\n\n// Review user active\nfunc (ar *ReviewActivityRepo) Review(ctx context.Context, act *schema.PassReviewActivity) (err error) {\n\tcfg, err := ar.configService.GetConfigByKey(ctx, EditAccepted)\n\tif err != nil {\n\t\treturn err\n\t}\n\taddActivity := &entity.Activity{\n\t\tUserID:           act.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(act.TriggerUserID),\n\t\tObjectID:         act.ObjectID,\n\t\tOriginalObjectID: act.OriginalObjectID,\n\t\tActivityType:     cfg.ID,\n\t\tRank:             cfg.GetIntValue(),\n\t\tHasRank:          1,\n\t\tRevisionID:       converter.StringToInt64(act.RevisionID),\n\t}\n\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuser := &entity.User{}\n\t\texist, err := session.ID(addActivity.UserID).ForUpdate().Get(user)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, fmt.Errorf(\"user not exist\")\n\t\t}\n\n\t\texistsActivity := &entity.Activity{}\n\t\texist, err = session.\n\t\t\tAnd(builder.Eq{\"user_id\": addActivity.UserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": addActivity.ActivityType}).\n\t\t\tAnd(builder.Eq{\"revision_id\": addActivity.RevisionID}).\n\t\t\tGet(existsActivity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\terr = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t_, err = session.Insert(addActivity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/activity/user_active_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// UserActiveActivityRepo answer accepted\ntype UserActiveActivityRepo struct {\n\tdata          *data.Data\n\tactivityRepo  activity_common.ActivityRepo\n\tuserRankRepo  rank.UserRankRepo\n\tconfigService *config.ConfigService\n}\n\nconst (\n\tUserActivated = \"user.activated\"\n)\n\n// NewUserActiveActivityRepo new repository\nfunc NewUserActiveActivityRepo(\n\tdata *data.Data,\n\tactivityRepo activity_common.ActivityRepo,\n\tuserRankRepo rank.UserRankRepo,\n\tconfigService *config.ConfigService,\n) activity.UserActiveActivityRepo {\n\treturn &UserActiveActivityRepo{\n\t\tdata:          data,\n\t\tactivityRepo:  activityRepo,\n\t\tuserRankRepo:  userRankRepo,\n\t\tconfigService: configService,\n\t}\n}\n\n// UserActive user active\nfunc (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) {\n\tcfg, err := ar.configService.GetConfigByKey(ctx, UserActivated)\n\tif err != nil {\n\t\treturn err\n\t}\n\taddActivity := &entity.Activity{\n\t\tUserID:           userID,\n\t\tObjectID:         \"0\",\n\t\tOriginalObjectID: \"0\",\n\t\tActivityType:     cfg.ID,\n\t\tRank:             cfg.GetIntValue(),\n\t\tHasRank:          1,\n\t}\n\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuser := &entity.User{}\n\t\texist, err := session.ID(userID).ForUpdate().Get(user)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, fmt.Errorf(\"user not exist\")\n\t\t}\n\n\t\texistsActivity := &entity.Activity{}\n\t\texist, err = session.\n\t\t\tAnd(builder.Eq{\"user_id\": addActivity.UserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": addActivity.ActivityType}).\n\t\t\tGet(existsActivity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\terr = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t_, err = session.Insert(addActivity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/activity/vote_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/pkg/converter\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/pkg/obj\"\n\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// VoteRepo activity repository\ntype VoteRepo struct {\n\tdata                     *data.Data\n\tactivityRepo             activity_common.ActivityRepo\n\tuserRankRepo             rank.UserRankRepo\n\tnotificationQueueService noticequeue.Service\n}\n\n// NewVoteRepo new repository\nfunc NewVoteRepo(\n\tdata *data.Data,\n\tactivityRepo activity_common.ActivityRepo,\n\tuserRankRepo rank.UserRankRepo,\n\tnotificationQueueService noticequeue.Service,\n) content.VoteRepo {\n\treturn &VoteRepo{\n\t\tdata:                     data,\n\t\tactivityRepo:             activityRepo,\n\t\tuserRankRepo:             userRankRepo,\n\t\tnotificationQueueService: notificationQueueService,\n\t}\n}\n\nfunc (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {\n\tnoNeedToVote, err := vr.votePreCheck(ctx, op)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif noNeedToVote {\n\t\treturn nil\n\t}\n\n\tsendInboxNotification := false\n\tmaxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar userIDs []string\n\tfor _, activity := range op.Activities {\n\t\tuserIDs = append(userIDs, activity.ActivityUserID)\n\t}\n\n\t_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuserInfoMapping, err := vr.acquireUserInfo(session, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, userInfoMapping, maxDailyRank)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tsendInboxNotification, err = vr.saveActivitiesAvailable(session, op)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = vr.changeUserRank(ctx, session, op, userInfoMapping)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, activity := range op.Activities {\n\t\tif activity.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)\n\t}\n\tif sendInboxNotification {\n\t\tvr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)\n\t}\n\treturn nil\n}\n\nfunc (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {\n\t// Pre-Check\n\t// 1. check if the activity exist\n\t// 2. check if the activity is not cancelled\n\t// 3. if all activities are cancelled, return directly\n\tactivities, err := vr.getExistActivity(ctx, op)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar userIDs []string\n\tfor _, activity := range activities {\n\t\tif activity.Cancelled == entity.ActivityCancelled {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, activity.UserID)\n\t}\n\tif len(userIDs) == 0 {\n\t\treturn nil\n\t}\n\n\t_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tuserInfoMapping, err := vr.acquireUserInfo(session, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = vr.cancelActivities(session, activities)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, activity := range activities {\n\t\tif activity.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)\n\t}\n\treturn nil\n}\n\nfunc (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (\n\tup, down int64, err error) {\n\tup = vr.countVoteUp(ctx, objectID, objectType)\n\tdown = vr.countVoteDown(ctx, objectID, objectType)\n\terr = vr.updateVotes(ctx, objectID, objectType, int(up-down))\n\treturn\n}\n\nfunc (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,\n\tpage int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {\n\tsession := vr.data.DB.Context(ctx)\n\tcond := builder.\n\t\tAnd(\n\t\t\tbuilder.Eq{\"user_id\": userID},\n\t\t\tbuilder.Eq{\"cancelled\": 0},\n\t\t\tbuilder.In(\"activity_type\", activityTypes),\n\t\t)\n\n\tsession.Where(cond).Desc(\"updated_at\")\n\n\ttotal, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {\n\tactivities, err := vr.getExistActivity(ctx, op)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdone := 0\n\tfor _, activity := range activities {\n\t\tif activity.Cancelled == entity.ActivityAvailable {\n\t\t\tdone++\n\t\t}\n\t}\n\treturn done == len(op.Activities), nil\n}\n\nfunc (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {\n\tus := make([]*entity.User, 0)\n\terr := session.In(\"id\", userIDs).ForUpdate().Find(&us)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn nil, err\n\t}\n\n\tusers := make(map[string]*entity.User, 0)\n\tfor _, u := range us {\n\t\tusers[u.ID] = u\n\t}\n\treturn users, nil\n}\n\nfunc (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,\n\top *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) {\n\t// check if user reach daily rank limit\n\tfor _, activity := range op.Activities {\n\t\tif userInfoMapping[activity.ActivityUserID] == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif activity.Rank > 0 {\n\t\t\t// check if reach max daily rank\n\t\t\treach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif reach {\n\t\t\t\tactivity.Rank = 0\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\t// If user rank is lower than 1 after this action, then user rank will be set to 1 only.\n\t\t\tuserCurrentScore := userInfoMapping[activity.ActivityUserID].Rank\n\t\t\tif userCurrentScore+activity.Rank < 1 {\n\t\t\t\tactivity.Rank = 1 - userCurrentScore\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,\n\top *schema.VoteOperationInfo,\n\tuserInfoMapping map[string]*entity.User) (err error) {\n\tfor _, activity := range op.Activities {\n\t\tif activity.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tuser := userInfoMapping[activity.ActivityUserID]\n\t\tif user == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err = vr.userRankRepo.ChangeUserRank(ctx, session,\n\t\t\tactivity.ActivityUserID, user.Rank, activity.Rank); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,\n\tactivities []*entity.Activity,\n\tuserInfoMapping map[string]*entity.User) (err error) {\n\tfor _, activity := range activities {\n\t\tif activity.Rank == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tuser := userInfoMapping[activity.UserID]\n\t\tif user == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err = vr.userRankRepo.ChangeUserRank(ctx, session,\n\t\t\tactivity.UserID, user.Rank, -activity.Rank); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// saveActivitiesAvailable save activities\n// If activity not exist it will be created or else will be updated\n// If this activity is already exist, set activity rank to 0\n// So after this function, the activity rank will be correct for update user rank\nfunc (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {\n\tfor _, activity := range op.Activities {\n\t\texistsActivity := &entity.Activity{}\n\t\texist, err := session.\n\t\t\tWhere(builder.Eq{\"object_id\": op.ObjectID}).\n\t\t\tAnd(builder.Eq{\"user_id\": activity.ActivityUserID}).\n\t\t\tAnd(builder.Eq{\"trigger_user_id\": activity.TriggerUserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": activity.ActivityType}).\n\t\t\tGet(existsActivity)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif exist && existsActivity.Cancelled == entity.ActivityAvailable {\n\t\t\tactivity.Rank = 0\n\t\t\tcontinue\n\t\t}\n\t\tif exist {\n\t\t\tbean := &entity.Activity{\n\t\t\t\tCancelled: entity.ActivityAvailable,\n\t\t\t\tRank:      activity.Rank,\n\t\t\t\tHasRank:   activity.HasRank(),\n\t\t\t}\n\t\t\tsession.Where(\"id = ?\", existsActivity.ID)\n\t\t\tif _, err = session.Cols(\"`cancelled`\", \"`rank`\", \"`has_rank`\").\n\t\t\t\tUpdate(bean); err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t} else {\n\t\t\tinsertActivity := entity.Activity{\n\t\t\t\tObjectID:         op.ObjectID,\n\t\t\t\tOriginalObjectID: op.ObjectID,\n\t\t\t\tUserID:           activity.ActivityUserID,\n\t\t\t\tTriggerUserID:    converter.StringToInt64(activity.TriggerUserID),\n\t\t\t\tActivityType:     activity.ActivityType,\n\t\t\t\tRank:             activity.Rank,\n\t\t\t\tHasRank:          activity.HasRank(),\n\t\t\t\tCancelled:        entity.ActivityAvailable,\n\t\t\t}\n\t\t\t_, err = session.Insert(&insertActivity)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tnewAct = true\n\t\t}\n\t}\n\treturn newAct, nil\n}\n\n// cancelActivities cancel activities\n// If this activity is already cancelled, set activity rank to 0\n// So after this function, the activity rank will be correct for update user rank\nfunc (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {\n\tfor _, activity := range activities {\n\t\tt := &entity.Activity{}\n\t\texist, err := session.ID(activity.ID).Get(t)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\tlog.Error(fmt.Errorf(\"%s activity not exist\", activity.ID))\n\t\t\treturn fmt.Errorf(\"%s activity not exist\", activity.ID)\n\t\t}\n\t\t//  If this activity is already cancelled, set activity rank to 0\n\t\tif t.Cancelled == entity.ActivityCancelled {\n\t\t\tactivity.Rank = 0\n\t\t}\n\t\tif _, err = session.ID(activity.ID).Cols(\"cancelled\", \"cancelled_at\").\n\t\t\tUpdate(&entity.Activity{\n\t\t\t\tCancelled:   entity.ActivityCancelled,\n\t\t\t\tCancelledAt: time.Now(),\n\t\t\t}); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {\n\tvar activities []*entity.Activity\n\tfor _, action := range op.Activities {\n\t\tt := &entity.Activity{}\n\t\texist, err := vr.data.DB.Context(ctx).\n\t\t\tWhere(builder.Eq{\"user_id\": action.ActivityUserID}).\n\t\t\tAnd(builder.Eq{\"trigger_user_id\": action.TriggerUserID}).\n\t\t\tAnd(builder.Eq{\"activity_type\": action.ActivityType}).\n\t\t\tAnd(builder.Eq{\"object_id\": op.ObjectID}).\n\t\t\tGet(t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif exist {\n\t\t\tactivities = append(activities, t)\n\t\t}\n\t}\n\treturn activities, nil\n}\n\nfunc (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {\n\tcount, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)\n\tif err != nil {\n\t\tlog.Errorf(\"get vote up count error: %v\", err)\n\t}\n\treturn count\n}\n\nfunc (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {\n\tcount, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)\n\tif err != nil {\n\t\tlog.Errorf(\"get vote down count error: %v\", err)\n\t}\n\treturn count\n}\n\nfunc (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {\n\tactivity := &entity.Activity{}\n\tactivityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)\n\tcount, err = vr.data.DB.Context(ctx).Where(builder.Eq{\"object_id\": objectID}).\n\t\tAnd(builder.Eq{\"activity_type\": activityType}).\n\t\tAnd(builder.Eq{\"cancelled\": 0}).\n\t\tCount(activity)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, err\n}\n\nfunc (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {\n\tsession := vr.data.DB.Context(ctx)\n\tswitch objectType {\n\tcase constant.QuestionObjectType:\n\t\t_, err = session.ID(objectID).Cols(\"vote_count\").Update(&entity.Question{VoteCount: voteCount})\n\tcase constant.AnswerObjectType:\n\t\t_, err = session.ID(objectID).Cols(\"vote_count\").Update(&entity.Answer{VoteCount: voteCount})\n\tcase constant.CommentObjectType:\n\t\t_, err = session.ID(objectID).Cols(\"vote_count\").Update(&entity.Comment{VoteCount: voteCount})\n\t}\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn\n}\n\nfunc (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: activityUserID,\n\t\tTriggerUserID:  objectUserID,\n\t\tType:           schema.NotificationTypeAchievement,\n\t\tObjectID:       objectID,\n\t\tObjectType:     objectType,\n\t}\n\tvr.notificationQueueService.Send(ctx, msg)\n}\n\nfunc (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) {\n\tif triggerUserID == receiverUserID {\n\t\treturn\n\t}\n\tobjectType, _ := obj.GetObjectTypeStrByObjectID(objectID)\n\n\tmsg := &schema.NotificationMsg{\n\t\tTriggerUserID:  triggerUserID,\n\t\tReceiverUserID: receiverUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       objectID,\n\t\tObjectType:     objectType,\n\t}\n\tif objectType == constant.QuestionObjectType {\n\t\tif upvote {\n\t\t\tmsg.NotificationAction = constant.NotificationUpVotedTheQuestion\n\t\t} else {\n\t\t\tmsg.NotificationAction = constant.NotificationDownVotedTheQuestion\n\t\t}\n\t}\n\tif objectType == constant.AnswerObjectType {\n\t\tif upvote {\n\t\t\tmsg.NotificationAction = constant.NotificationUpVotedTheAnswer\n\t\t} else {\n\t\t\tmsg.NotificationAction = constant.NotificationDownVotedTheAnswer\n\t\t}\n\t}\n\tif objectType == constant.CommentObjectType {\n\t\tif upvote {\n\t\t\tmsg.NotificationAction = constant.NotificationUpVotedTheComment\n\t\t}\n\t}\n\tif len(msg.NotificationAction) > 0 {\n\t\tvr.notificationQueueService.Send(ctx, msg)\n\t}\n}\n"
  },
  {
    "path": "internal/repo/activity_common/activity_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activity_type\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// ActivityRepo activity repository\ntype ActivityRepo struct {\n\tdata          *data.Data\n\tuniqueIDRepo  unique.UniqueIDRepo\n\tconfigService *config.ConfigService\n}\n\n// NewActivityRepo new repository\nfunc NewActivityRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n\tconfigService *config.ConfigService,\n) activity_common.ActivityRepo {\n\treturn &ActivityRepo{\n\t\tdata:          data,\n\t\tuniqueIDRepo:  uniqueIDRepo,\n\t\tconfigService: configService,\n\t}\n}\n\nfunc (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (\n\tactivityType, rank, hasRank int, err error) {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tconfKey := fmt.Sprintf(\"%s.%s\", objectType, action)\n\tcfg, err := ar.configService.GetConfigByKey(ctx, confKey)\n\tif err != nil {\n\t\treturn\n\t}\n\tactivityType, rank = cfg.ID, cfg.GetIntValue()\n\thasRank = 0\n\tif rank != 0 {\n\t\thasRank = 1\n\t}\n\treturn\n}\n\nfunc (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {\n\tconfigKey := fmt.Sprintf(\"%s.%s\", objectType, action)\n\tcfg, err := ar.configService.GetConfigByKey(ctx, configKey)\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn cfg.ID, nil\n}\n\nfunc (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) {\n\tcfg, err := ar.configService.GetConfigByKey(ctx, configKey)\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn cfg.ID, nil\n}\n\nfunc (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session,\n\tobjectID, userID string, activityType int,\n) (existsActivity *entity.Activity, exist bool, err error) {\n\texistsActivity = &entity.Activity{}\n\texist, err = session.\n\t\tWhere(builder.Eq{\"object_id\": objectID}).\n\t\tAnd(builder.Eq{\"user_id\": userID}).\n\t\tAnd(builder.Eq{\"activity_type\": activityType}).\n\t\tGet(existsActivity)\n\treturn\n}\n\nfunc (ar *ActivityRepo) GetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) (\n\tactivityList []*entity.Activity, err error) {\n\tactivityList = make([]*entity.Activity, 0)\n\terr = ar.data.DB.Context(ctx).Where(\"user_id = ?\", userID).\n\t\tAnd(\"activity_type = ?\", activityType).\n\t\tAnd(\"cancelled = 0\").\n\t\tFind(&activityList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) {\n\tsum := &entity.ActivityRankSum{}\n\t_, err := ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()).\n\t\tSelect(\"sum(`rank`) as `rank`\").\n\t\tWhere(\"user_id =?\", userID).\n\t\tAnd(\"object_id = ?\", objectID).\n\t\tAnd(\"cancelled =0\").\n\t\tGet(sum)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn 0, err\n\t}\n\treturn sum.Rank, nil\n}\n\n// AddActivity add activity\nfunc (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) {\n\t_, err = ar.data.DB.Context(ctx).Insert(activity)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time\nfunc (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(\n\tctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {\n\trankStat = make([]*entity.ActivityUserRankStat, 0)\n\tsession := ar.data.DB.Context(ctx).Select(\"user_id, SUM(`rank`) AS rank_amount\").Table(\"activity\")\n\tsession.Where(\"has_rank = 1 AND cancelled = 0\")\n\tsession.Where(\"created_at >= ?\", startTime)\n\tsession.Where(\"created_at <= ?\", endTime)\n\tsession.GroupBy(\"user_id\")\n\tsession.Desc(\"rank_amount\")\n\tsession.Limit(limit)\n\terr = session.Find(&rankStat)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUsersWhoHasVoteMost get users who has vote most\nfunc (ar *ActivityRepo) GetUsersWhoHasVoteMost(\n\tctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) {\n\tvoteStat = make([]*entity.ActivityUserVoteStat, 0)\n\n\tactIDs := make([]int, 0)\n\tfor _, act := range activity_type.ActivityTypeList {\n\t\tcfg, err := ar.configService.GetConfigByKey(ctx, act)\n\t\tif err == nil {\n\t\t\tactIDs = append(actIDs, cfg.ID)\n\t\t}\n\t}\n\n\tsession := ar.data.DB.Context(ctx).Select(\"user_id, COUNT(*) AS vote_count\").Table(\"activity\")\n\tsession.Where(\"cancelled = 0\")\n\tsession.In(\"activity_type\", actIDs)\n\tsession.Where(\"created_at >= ?\", startTime)\n\tsession.Where(\"created_at <= ?\", endTime)\n\tsession.GroupBy(\"user_id\")\n\tsession.Desc(\"vote_count\")\n\tsession.Limit(limit)\n\terr = session.Find(&voteStat)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/activity_common/follow.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// FollowRepo follow repository\ntype FollowRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n\tactivityRepo activity_common.ActivityRepo\n}\n\n// NewFollowRepo new repository\nfunc NewFollowRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n\tactivityRepo activity_common.ActivityRepo,\n) activity_common.FollowRepo {\n\treturn &FollowRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t\tactivityRepo: activityRepo,\n\t}\n}\n\n// GetFollowAmount get object id's follows\nfunc (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch objectType {\n\tcase \"question\":\n\t\tmodel := &entity.Question{}\n\t\t_, err = ar.data.DB.Context(ctx).Where(\"id = ?\", objectID).Cols(\"`follow_count`\").Get(model)\n\t\tif err == nil {\n\t\t\tfollows = model.FollowCount\n\t\t}\n\tcase \"user\":\n\t\tmodel := &entity.User{}\n\t\t_, err = ar.data.DB.Context(ctx).Where(\"id = ?\", objectID).Cols(\"`follow_count`\").Get(model)\n\t\tif err == nil {\n\t\t\tfollows = model.FollowCount\n\t\t}\n\tcase \"tag\":\n\t\tmodel := &entity.Tag{}\n\t\t_, err = ar.data.DB.Context(ctx).Where(\"id = ?\", objectID).Cols(\"`follow_count`\").Get(model)\n\t\tif err == nil {\n\t\t\tfollows = model.FollowCount\n\t\t}\n\tdefault:\n\t\terr = errors.InternalServer(reason.DisallowFollow).WithMsg(\"this object can't be followed\")\n\t}\n\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn follows, nil\n}\n\n// GetFollowUserIDs get follow userID by objectID\nfunc (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) {\n\tobjectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, \"follow\")\n\tif err != nil {\n\t\tlog.Errorf(\"can't get activity type by object key: %s\", objectTypeStr)\n\t\treturn nil, err\n\t}\n\n\tuserIDs = make([]string, 0)\n\tsession := ar.data.DB.Context(ctx).Select(\"user_id\")\n\tsession.Table(entity.Activity{}.TableName())\n\tsession.Where(\"object_id = ?\", objectID)\n\tsession.Where(\"activity_type = ?\", activityType)\n\tsession.Where(\"cancelled = 0\")\n\terr = session.Find(&userIDs)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn userIDs, nil\n}\n\n// GetFollowIDs get all follow id list\nfunc (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) {\n\tfollowIDs = make([]string, 0)\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, \"follow\")\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tsession := ar.data.DB.Context(ctx).Select(\"object_id\")\n\tsession.Table(entity.Activity{}.TableName())\n\tsession.Where(\"user_id = ? AND activity_type = ?\", userID, activityType)\n\tsession.Where(\"cancelled = 0\")\n\terr = session.Find(&followIDs)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn followIDs, nil\n}\n\n// IsFollowed check user if follow object or not\nfunc (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (followed bool, err error) {\n\tobjectKey, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, \"follow\")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tat := &entity.Activity{}\n\thas, err := ar.data.DB.Context(ctx).Where(\"user_id = ? AND object_id = ? AND activity_type = ?\", userID, objectID, activityType).Get(at)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !has {\n\t\treturn false, nil\n\t}\n\tif at.Cancelled == entity.ActivityCancelled {\n\t\treturn false, nil\n\t} else {\n\t\treturn true, nil\n\t}\n}\n\n// MigrateFollowers migrate followers from source object to target object\nfunc (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error {\n\t// if source object id and target object id are same type\n\tsourceObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(sourceObjectID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\ttargetObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(targetObjectID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif sourceObjectTypeStr != targetObjectTypeStr {\n\t\treturn errors.InternalServer(reason.DisallowFollow).WithMsg(\"not same object type\")\n\t}\n\tactivityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, sourceObjectTypeStr, action)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 1. get all user ids who follow the source object\n\tuserIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID)\n\tif err != nil {\n\t\tlog.Errorf(\"MigrateFollowers: failed to get user ids who follow %s: %v\", sourceObjectID, err)\n\t\treturn err\n\t}\n\n\t_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\t// 1. delete all follows of the source object\n\t\t_, err = session.Table(entity.Activity{}.TableName()).\n\t\t\tWhere(builder.Eq{\n\t\t\t\t\"object_id\":     sourceObjectID,\n\t\t\t\t\"activity_type\": activityType,\n\t\t\t}).\n\t\t\tDelete(&entity.Activity{})\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\t// 2. update cancel status to active for target tag if source tag followers is active\n\t\t_, err = session.Table(entity.Activity{}.TableName()).\n\t\t\tWhere(builder.Eq{\n\t\t\t\t\"object_id\":     targetObjectID,\n\t\t\t\t\"activity_type\": activityType,\n\t\t\t}).\n\t\t\tAnd(builder.In(\"user_id\", userIDs)).\n\t\t\tCols(\"cancelled\").\n\t\t\tUpdate(&entity.Activity{\n\t\t\t\tCancelled: entity.ActivityAvailable,\n\t\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\t// 3. get existing follows of the target object\n\t\ttargetFollowers := make([]string, 0)\n\t\terr = session.Table(entity.Activity{}.TableName()).\n\t\t\tWhere(builder.Eq{\n\t\t\t\t\"object_id\":     targetObjectID,\n\t\t\t\t\"activity_type\": activityType,\n\t\t\t\t\"cancelled\":     entity.ActivityAvailable,\n\t\t\t}).\n\t\t\tCols(\"user_id\").\n\t\t\tFind(&targetFollowers)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\t// 4. filter out user ids that already follow the target object and create new activity\n\t\t// Create a map for faster lookup of existing followers\n\t\texistingFollowers := make(map[string]bool)\n\t\tfor _, uid := range targetFollowers {\n\t\t\texistingFollowers[uid] = true\n\t\t}\n\n\t\t// Filter out users who already follow the target\n\t\tnewFollowers := make([]string, 0)\n\t\tfor _, uid := range userIDs {\n\t\t\tif !existingFollowers[uid] {\n\t\t\t\tnewFollowers = append(newFollowers, uid)\n\t\t\t}\n\t\t}\n\n\t\t// Create new activities for the filtered users\n\t\tfor _, uid := range newFollowers {\n\t\t\tactivity := &entity.Activity{\n\t\t\t\tUserID:           uid,\n\t\t\t\tObjectID:         targetObjectID,\n\t\t\t\tOriginalObjectID: targetObjectID,\n\t\t\t\tActivityType:     activityType,\n\t\t\t\tCreatedAt:        time.Now(),\n\t\t\t\tUpdatedAt:        time.Now(),\n\t\t\t\tCancelled:        entity.ActivityAvailable,\n\t\t\t}\n\t\t\tif _, err = session.Insert(activity); err != nil {\n\t\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t\t}\n\t\t}\n\t\treturn nil, nil\n\t})\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/repo/activity_common/vote.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/pkg/uid\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// VoteRepo activity repository\ntype VoteRepo struct {\n\tdata         *data.Data\n\tactivityRepo activity_common.ActivityRepo\n}\n\n// NewVoteRepo new repository\nfunc NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) activity_common.VoteRepo {\n\treturn &VoteRepo{\n\t\tdata:         data,\n\t\tactivityRepo: activityRepo,\n\t}\n}\n\nfunc (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) {\n\tif len(userID) == 0 {\n\t\treturn \"\"\n\t}\n\tobjectID = uid.DeShortID(objectID)\n\tif len(objectID) == 0 || objectID == \"0\" {\n\t\treturn \"\"\n\t}\n\tfor _, action := range []string{\"vote_up\", \"vote_down\"} {\n\t\tactivityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tat := &entity.Activity{}\n\t\thas, err := vr.data.DB.Context(ctx).Where(\"object_id = ? AND cancelled = 0 AND activity_type = ? AND user_id = ?\",\n\t\t\tobjectID, activityType, userID).Get(at)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn \"\"\n\t\t}\n\t\tif has {\n\t\t\treturn action\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) {\n\tlist := make([]*entity.Activity, 0)\n\tcount, err = vr.data.DB.Context(ctx).Where(\"cancelled =0\").In(\"activity_type\", activityTypes).FindAndCount(&list)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/ai_conversation/ai_conversation_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage ai_conversation\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// AIConversationRepo\ntype AIConversationRepo interface {\n\tCreateConversation(ctx context.Context, conversation *entity.AIConversation) error\n\tGetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error)\n\tUpdateConversation(ctx context.Context, conversation *entity.AIConversation) error\n\tGetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error)\n\tCreateRecord(ctx context.Context, record *entity.AIConversationRecord) error\n\tGetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error)\n\tUpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) error\n\tGetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error)\n\tGetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error)\n\tGetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error)\n\tGetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error)\n\tDeleteConversation(ctx context.Context, conversationID string) error\n}\n\ntype aiConversationRepo struct {\n\tdata *data.Data\n}\n\n// NewAIConversationRepo new AIConversationRepo\nfunc NewAIConversationRepo(data *data.Data) AIConversationRepo {\n\treturn &aiConversationRepo{\n\t\tdata: data,\n\t}\n}\n\n// CreateConversation creates a conversation\nfunc (r *aiConversationRepo) CreateConversation(ctx context.Context, conversation *entity.AIConversation) error {\n\t_, err := r.data.DB.Context(ctx).Insert(conversation)\n\tif err != nil {\n\t\tlog.Errorf(\"create ai conversation failed: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConversation gets a conversation\nfunc (r *aiConversationRepo) GetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error) {\n\tconversation := &entity.AIConversation{}\n\texist, err := r.data.DB.Context(ctx).Where(builder.Eq{\"conversation_id\": conversationID}).Get(conversation)\n\tif err != nil {\n\t\tlog.Errorf(\"get ai conversation failed: %v\", err)\n\t\treturn nil, false, err\n\t}\n\treturn conversation, exist, nil\n}\n\n// UpdateConversation updates a conversation\nfunc (r *aiConversationRepo) UpdateConversation(ctx context.Context, conversation *entity.AIConversation) error {\n\t_, err := r.data.DB.Context(ctx).ID(conversation.ID).Update(conversation)\n\tif err != nil {\n\t\tlog.Errorf(\"update ai conversation failed: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetConversationsPage get conversations by user ID\nfunc (r *aiConversationRepo) GetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) {\n\tlist = make([]*entity.AIConversation, 0)\n\ttotal, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc(\"id\"))\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn list, total, err\n}\n\n// CreateRecord creates a conversation record\nfunc (r *aiConversationRepo) CreateRecord(ctx context.Context, record *entity.AIConversationRecord) error {\n\t_, err := r.data.DB.Context(ctx).Insert(record)\n\tif err != nil {\n\t\tlog.Errorf(\"create ai conversation record failed: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetRecordsByConversationID get records by conversation ID\nfunc (r *aiConversationRepo) GetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error) {\n\trecords := make([]*entity.AIConversationRecord, 0)\n\terr := r.data.DB.Context(ctx).\n\t\tWhere(builder.Eq{\"conversation_id\": conversationID}).\n\t\tOrderBy(\"created_at ASC\").\n\t\tFind(&records)\n\tif err != nil {\n\t\tlog.Errorf(\"get ai conversation records failed: %v\", err)\n\t\treturn nil, err\n\t}\n\treturn records, nil\n}\n\n// UpdateRecordVote update record vote\nfunc (r *aiConversationRepo) UpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) (err error) {\n\t_, err = r.data.DB.Context(ctx).ID(cond.ID).MustCols(\"helpful\", \"unhelpful\").Update(cond)\n\tif err != nil {\n\t\tlog.Errorf(\"update ai conversation record vote failed: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetRecord get record\nfunc (r *aiConversationRepo) GetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error) {\n\trecord := &entity.AIConversationRecord{}\n\texist, err := r.data.DB.Context(ctx).ID(recordID).Get(record)\n\tif err != nil {\n\t\tlog.Errorf(\"get ai conversation record failed: %v\", err)\n\t\treturn nil, false, err\n\t}\n\treturn record, exist, nil\n}\n\n// GetRecordByChatCompletionID gets record by chat completion ID\nfunc (r *aiConversationRepo) GetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error) {\n\trecord := &entity.AIConversationRecord{}\n\texist, err := r.data.DB.Context(ctx).Where(builder.Eq{\"role\": role}).\n\t\tWhere(builder.Eq{\"chat_completion_id\": chatCompletionID}).Get(record)\n\tif err != nil {\n\t\tlog.Errorf(\"get ai conversation record by chat completion id failed: %v\", err)\n\t\treturn nil, false, err\n\t}\n\treturn record, exist, nil\n}\n\n// GetConversationsForAdmin gets conversation list for admin\nfunc (r *aiConversationRepo) GetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) {\n\tlist = make([]*entity.AIConversation, 0)\n\ttotal, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc(\"id\"))\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn list, total, err\n}\n\n// GetConversationWithVoteStats gets conversation vote statistics\nfunc (r *aiConversationRepo) GetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error) {\n\tres, err := r.data.DB.Context(ctx).SumsInt(&entity.AIConversationRecord{ConversationID: conversationID}, \"helpful\", \"unhelpful\")\n\tif err != nil {\n\t\tlog.Errorf(\"get ai conversation vote stats failed: %v\", err)\n\t\treturn 0, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif len(res) < 2 {\n\t\tlog.Errorf(\"get ai conversation vote stats failed: invalid result length %d\", len(res))\n\t\treturn 0, 0, nil\n\t}\n\treturn res[0], res[1], nil\n}\n\n// DeleteConversation deletes a conversation and its related records\nfunc (r *aiConversationRepo) DeleteConversation(ctx context.Context, conversationID string) error {\n\t_, err := r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tif _, err := session.Context(ctx).Where(\"conversation_id = ?\", conversationID).Delete(&entity.AIConversationRecord{}); err != nil {\n\t\t\tlog.Errorf(\"delete ai conversation records failed: %v\", err)\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\tif _, err := session.Context(ctx).Where(\"conversation_id = ?\", conversationID).Delete(&entity.AIConversation{}); err != nil {\n\t\t\tlog.Errorf(\"delete ai conversation failed: %v\", err)\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\treturn nil, nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/answer/answer_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage answer\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// answerRepo answer repository\ntype answerRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n\tuserRankRepo rank.UserRankRepo\n\tactivityRepo activity_common.ActivityRepo\n}\n\n// NewAnswerRepo new repository\nfunc NewAnswerRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n\tuserRankRepo rank.UserRankRepo,\n\tactivityRepo activity_common.ActivityRepo,\n) answercommon.AnswerRepo {\n\treturn &answerRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t\tuserRankRepo: userRankRepo,\n\t\tactivityRepo: activityRepo,\n\t}\n}\n\n// AddAnswer add answer\nfunc (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) {\n\tanswer.QuestionID = uid.DeShortID(answer.QuestionID)\n\tID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName())\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tanswer.ID = ID\n\t_, err = ar.data.DB.Context(ctx).Insert(answer)\n\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tanswer.ID = uid.EnShortID(answer.ID)\n\t\tanswer.QuestionID = uid.EnShortID(answer.QuestionID)\n\t}\n\t_ = ar.updateSearch(ctx, answer.ID)\n\treturn nil\n}\n\n// RemoveAnswer delete answer\nfunc (ar *answerRepo) RemoveAnswer(ctx context.Context, answerID string) (err error) {\n\tanswerID = uid.DeShortID(answerID)\n\t_, err = ar.data.DB.Context(ctx).ID(answerID).Cols(\"status\").Update(&entity.Answer{\n\t\tStatus: entity.AnswerStatusDeleted,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = ar.updateSearch(ctx, answerID)\n\treturn nil\n}\n\n// RecoverAnswer recover answer\nfunc (ar *answerRepo) RecoverAnswer(ctx context.Context, answerID string) (err error) {\n\tanswerID = uid.DeShortID(answerID)\n\t_, err = ar.data.DB.Context(ctx).ID(answerID).Cols(\"status\").Update(&entity.Answer{\n\t\tStatus: entity.AnswerStatusAvailable,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = ar.updateSearch(ctx, answerID)\n\treturn nil\n}\n\n// RemoveAllUserAnswer remove all user answer\nfunc (ar *answerRepo) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) {\n\t// find all answer id that need to be deleted\n\tanswerIDs := make([]string, 0)\n\tsession := ar.data.DB.Context(ctx).Where(\"user_id = ?\", userID)\n\tsession.Where(\"status != ?\", entity.AnswerStatusDeleted)\n\terr = session.Select(\"id\").Table(\"answer\").Find(&answerIDs)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif len(answerIDs) == 0 {\n\t\treturn nil\n\t}\n\n\tlog.Infof(\"find %d answers need to be deleted for user %s\", len(answerIDs), userID)\n\n\t// delete all question\n\tsession = ar.data.DB.Context(ctx).Where(\"user_id = ?\", userID)\n\tsession.Where(\"status != ?\", entity.AnswerStatusDeleted)\n\t_, err = session.Cols(\"status\", \"updated_at\").Update(&entity.Answer{\n\t\tUpdatedAt: time.Now(),\n\t\tStatus:    entity.AnswerStatusDeleted,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// update search content\n\tfor _, id := range answerIDs {\n\t\t_ = ar.updateSearch(ctx, id)\n\t}\n\treturn nil\n}\n\n// UpdateAnswer update answer\nfunc (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) {\n\tanswer.ID = uid.DeShortID(answer.ID)\n\tanswer.QuestionID = uid.DeShortID(answer.QuestionID)\n\t_, err = ar.data.DB.Context(ctx).ID(answer.ID).Cols(cols...).Update(answer)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = ar.updateSearch(ctx, answer.ID)\n\treturn err\n}\n\nfunc (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) {\n\tanswerID = uid.DeShortID(answerID)\n\t_, err = ar.data.DB.Context(ctx).ID(answerID).Cols(\"status\").Update(&entity.Answer{Status: status})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = ar.updateSearch(ctx, answerID)\n\treturn\n}\n\n// GetAnswer get answer one\nfunc (ar *answerRepo) GetAnswer(ctx context.Context, id string) (\n\tanswer *entity.Answer, exist bool, err error,\n) {\n\tid = uid.DeShortID(id)\n\tanswer = &entity.Answer{}\n\texist, err = ar.data.DB.Context(ctx).ID(id).Get(answer)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tanswer.ID = uid.EnShortID(answer.ID)\n\t\tanswer.QuestionID = uid.EnShortID(answer.QuestionID)\n\t}\n\treturn\n}\n\n// GetAnswerCount count answer\nfunc (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) {\n\tvar resp = new(entity.Answer)\n\tcount, err = ar.data.DB.Context(ctx).Where(\"status = ?\", entity.AnswerStatusAvailable).Count(resp)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetAnswerList get answer list all\nfunc (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {\n\tanswerList = make([]*entity.Answer, 0)\n\tanswer.ID = uid.DeShortID(answer.ID)\n\tanswer.QuestionID = uid.DeShortID(answer.QuestionID)\n\terr = ar.data.DB.Context(ctx).Find(&answerList, answer)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range answerList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn\n}\n\n// GetAnswerPage get answer page\nfunc (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) {\n\tanswer.ID = uid.DeShortID(answer.ID)\n\tanswer.QuestionID = uid.DeShortID(answer.QuestionID)\n\tanswerList = make([]*entity.Answer, 0)\n\ttotal, err = pager.Help(page, pageSize, &answerList, answer, ar.data.DB.Context(ctx))\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range answerList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn\n}\n\n// UpdateAcceptedStatus update all accepted status of this question's answers\nfunc (ar *answerRepo) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error {\n\tacceptedAnswerID = uid.DeShortID(acceptedAnswerID)\n\tquestionID = uid.DeShortID(questionID)\n\n\t// update all this question's answer accepted status to false\n\t_, err := ar.data.DB.Context(ctx).Where(\"question_id = ?\", questionID).Cols(\"adopted\").Update(&entity.Answer{\n\t\tAccepted: schema.AnswerAcceptedFailed,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// if acceptedAnswerID is not empty, update accepted status to true\n\tif len(acceptedAnswerID) > 0 && acceptedAnswerID != \"0\" {\n\t\t_, err = ar.data.DB.Context(ctx).Where(\"id = ?\", acceptedAnswerID).Cols(\"adopted\").Update(&entity.Answer{\n\t\t\tAccepted: schema.AnswerAcceptedEnable,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t}\n\t_ = ar.updateSearch(ctx, acceptedAnswerID)\n\treturn nil\n}\n\n// GetByID\nfunc (ar *answerRepo) GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) {\n\tvar resp entity.Answer\n\tanswerID = uid.DeShortID(answerID)\n\thas, err := ar.data.DB.Context(ctx).ID(answerID).Get(&resp)\n\tif err != nil {\n\t\treturn &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tresp.ID = uid.EnShortID(resp.ID)\n\t\tresp.QuestionID = uid.EnShortID(resp.QuestionID)\n\t}\n\treturn &resp, has, nil\n}\n\nfunc (ar *answerRepo) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) {\n\tfor idx, answerID := range answerIDs {\n\t\tanswerIDs[idx] = uid.DeShortID(answerID)\n\t}\n\tvar resp = make([]*entity.Answer, 0)\n\terr := ar.data.DB.Context(ctx).In(\"id\", answerIDs).Find(&resp)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range resp {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (ar *answerRepo) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) {\n\tquestionID = uid.DeShortID(questionID)\n\tvar resp = new(entity.Answer)\n\tcount, err := ar.data.DB.Context(ctx).Where(\"question_id =? and  status = ?\", questionID, entity.AnswerStatusAvailable).Count(resp)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (ar *answerRepo) GetCountByUserID(ctx context.Context, userID string) (int64, error) {\n\tvar resp = new(entity.Answer)\n\tcount, err := ar.data.DB.Context(ctx).Where(\" user_id = ?  and  status = ?\", userID, entity.AnswerStatusAvailable).Count(resp)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (ar *answerRepo) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) {\n\tquestionID = uid.DeShortID(questionID)\n\tvar ids []string\n\tresp := make([]string, 0)\n\terr := ar.data.DB.Context(ctx).Table(entity.Answer{}.TableName()).Where(\"question_id =? and  user_id = ? and status = ?\", questionID, userID, entity.AnswerStatusAvailable).OrderBy(\"created_at ASC\").Cols(\"id\").Find(&ids)\n\tif err != nil {\n\t\treturn resp, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, id := range ids {\n\t\t\tresp = append(resp, uid.EnShortID(id))\n\t\t}\n\t} else {\n\t\tresp = ids\n\t}\n\treturn resp, nil\n}\n\n// SearchList\nfunc (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {\n\tif search.QuestionID != \"\" {\n\t\tsearch.QuestionID = uid.DeShortID(search.QuestionID)\n\t}\n\tsearch.ID = uid.DeShortID(search.ID)\n\tvar count int64\n\tvar err error\n\trows := make([]*entity.Answer, 0)\n\tif search.Page > 0 {\n\t\tsearch.Page--\n\t} else {\n\t\tsearch.Page = 0\n\t}\n\tif search.PageSize == 0 {\n\t\tsearch.PageSize = constant.DefaultPageSize\n\t}\n\toffset := search.Page * search.PageSize\n\tsession := ar.data.DB.Context(ctx)\n\n\tif search.QuestionID != \"\" {\n\t\tsession = session.And(\"question_id = ?\", search.QuestionID)\n\t}\n\tif len(search.UserID) > 0 {\n\t\tsession = session.And(\"user_id = ?\", search.UserID)\n\t}\n\tswitch search.Order {\n\tcase entity.AnswerSearchOrderByTime:\n\t\tsession = session.OrderBy(\"created_at desc\")\n\tcase entity.AnswerSearchOrderByTimeAsc:\n\t\tsession = session.OrderBy(\"created_at asc\")\n\tcase entity.AnswerSearchOrderByVote:\n\t\tsession = session.OrderBy(\"vote_count desc\")\n\tdefault:\n\t\tsession = session.OrderBy(\"adopted desc,vote_count desc,created_at asc\")\n\t}\n\tif !search.IncludeDeleted {\n\t\tif search.LoginUserID == \"\" {\n\t\t\tsession = session.And(\"status = ? \", entity.AnswerStatusAvailable)\n\t\t} else {\n\t\t\tsession = session.And(\"status = ? OR user_id = ?\", entity.AnswerStatusAvailable, search.LoginUserID)\n\t\t}\n\t}\n\n\tsession = session.Limit(search.PageSize, offset)\n\tcount, err = session.FindAndCount(&rows)\n\tif err != nil {\n\t\treturn rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range rows {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn rows, count, nil\n}\n\n// GetPersonalAnswerPage personal answer page\nfunc (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) (\n\tresp []*entity.Answer, total int64, err error) {\n\tcond := &entity.Answer{\n\t\tUserID: req.UserID,\n\t}\n\tsession := ar.data.DB.Context(ctx)\n\tswitch req.Order {\n\tcase entity.AnswerSearchOrderByTime:\n\t\tsession = session.OrderBy(\"created_at desc\")\n\tcase entity.AnswerSearchOrderByTimeAsc:\n\t\tsession = session.OrderBy(\"created_at asc\")\n\tcase entity.AnswerSearchOrderByVote:\n\t\tsession = session.OrderBy(\"vote_count desc\")\n\tdefault:\n\t\tsession = session.OrderBy(\"adopted desc,vote_count desc,created_at asc\")\n\t}\n\tif req.ShowPending {\n\t\tsession = session.And(\"status != ?\", entity.AnswerStatusDeleted)\n\t} else {\n\t\tsession = session.And(\"status = ?\", entity.AnswerStatusAvailable)\n\t}\n\tresp = make([]*entity.Answer, 0)\n\ttotal, err = pager.Help(req.Page, req.PageSize, &resp, cond, session)\n\tif err != nil {\n\t\treturn nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range resp {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn resp, total, nil\n}\n\nfunc (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) (\n\tresp []*entity.Answer, total int64, err error) {\n\tcond := &entity.Answer{}\n\tsession := ar.data.DB.Context(ctx)\n\tif len(req.QuestionID) == 0 && len(req.AnswerID) == 0 {\n\t\tsession.Join(\"INNER\", \"question\", \"answer.question_id = question.id\")\n\t\tif len(req.QuestionTitle) > 0 {\n\t\t\tsession.Where(\"question.title like ?\", \"%\"+req.QuestionTitle+\"%\")\n\t\t}\n\t}\n\tif len(req.AnswerID) > 0 {\n\t\tcond.ID = req.AnswerID\n\t}\n\tif len(req.QuestionID) > 0 {\n\t\tsession.Where(\"answer.question_id = ?\", req.QuestionID)\n\t}\n\tif req.Status > 0 {\n\t\tcond.Status = req.Status\n\t}\n\tsession.Desc(\"answer.created_at\")\n\n\tresp = make([]*entity.Answer, 0)\n\ttotal, err = pager.Help(req.Page, req.PageSize, &resp, cond, session)\n\tif err != nil {\n\t\treturn nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn resp, total, nil\n}\n\n// SumVotesByQuestionID sum votes by question id\nfunc (ar *answerRepo) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) {\n\tquestionID = uid.DeShortID(questionID)\n\tvar resp entity.Answer\n\tcount, err := ar.data.DB.Context(ctx).Where(\"question_id = ? and status = ?\", questionID, entity.AnswerStatusAvailable).Sum(&resp, \"vote_count\")\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\n// updateSearch update search, if search plugin not enable, do nothing\nfunc (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) {\n\tanswerID = uid.DeShortID(answerID)\n\t// check search plugin\n\tvar (\n\t\ts plugin.Search\n\t)\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\ts = search\n\t\treturn nil\n\t})\n\tif s == nil {\n\t\treturn\n\t}\n\tanswer, exist, err := ar.GetAnswer(ctx, answerID)\n\tif !exist {\n\t\treturn\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// get question\n\tvar (\n\t\tquestion = new(entity.Question)\n\t)\n\texist, err = ar.data.DB.Context(ctx).Where(\"id = ?\", answer.QuestionID).Get(&question)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn\n\t}\n\n\t// get tags\n\tvar (\n\t\ttagListList = make([]*entity.TagRel, 0)\n\t\ttags        = make([]string, 0)\n\t)\n\tst := ar.data.DB.Context(ctx).Where(\"object_id = ?\", uid.DeShortID(question.ID))\n\tst.Where(\"status = ?\", entity.TagRelStatusAvailable)\n\terr = st.Find(&tagListList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tfor _, tag := range tagListList {\n\t\ttags = append(tags, tag.TagID)\n\t}\n\n\tcontent := &plugin.SearchContent{\n\t\tObjectID:    answerID,\n\t\tTitle:       question.Title,\n\t\tType:        constant.AnswerObjectType,\n\t\tContent:     answer.OriginalText,\n\t\tAnswers:     0,\n\t\tStatus:      plugin.SearchContentStatus(answer.Status),\n\t\tTags:        tags,\n\t\tQuestionID:  answer.QuestionID,\n\t\tUserID:      answer.UserID,\n\t\tViews:       int64(question.ViewCount),\n\t\tCreated:     answer.CreatedAt.Unix(),\n\t\tActive:      answer.UpdatedAt.Unix(),\n\t\tScore:       int64(answer.VoteCount),\n\t\tHasAccepted: answer.Accepted == schema.AnswerAcceptedEnable,\n\t}\n\terr = s.UpdateContent(ctx, content)\n\treturn\n}\n\nfunc (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error {\n\t// get all deleted answers ids\n\tids := make([]string, 0)\n\terr := ar.data.DB.Context(ctx).Select(\"id\").Table(new(entity.Answer).TableName()).\n\t\tWhere(\"status = ?\", entity.AnswerStatusDeleted).Find(&ids)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil\n\t}\n\n\t// delete all revisions permanently\n\t_, err = ar.data.DB.Context(ctx).In(\"object_id\", ids).Delete(&entity.Revision{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t_, err = ar.data.DB.Context(ctx).Where(\"status = ?\", entity.AnswerStatusDeleted).Delete(&entity.Answer{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/api_key/api_key_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage api_key\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/apikey\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\ntype apiKeyRepo struct {\n\tdata *data.Data\n}\n\n// NewAPIKeyRepo creates a new apiKey repository\nfunc NewAPIKeyRepo(data *data.Data) apikey.APIKeyRepo {\n\treturn &apiKeyRepo{\n\t\tdata: data,\n\t}\n}\n\nfunc (ar *apiKeyRepo) GetAPIKeyList(ctx context.Context) (keys []*entity.APIKey, err error) {\n\tkeys = make([]*entity.APIKey, 0)\n\terr = ar.data.DB.Context(ctx).Where(\"hidden = ?\", 0).Find(&keys)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ar *apiKeyRepo) GetAPIKey(ctx context.Context, apiKey string) (key *entity.APIKey, exist bool, err error) {\n\tkey = &entity.APIKey{}\n\texist, err = ar.data.DB.Context(ctx).Where(\"access_key = ?\", apiKey).Get(key)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ar *apiKeyRepo) UpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) {\n\t_, err = ar.data.DB.Context(ctx).ID(apiKey.ID).Update(&apiKey)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ar *apiKeyRepo) AddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) {\n\t_, err = ar.data.DB.Context(ctx).Insert(&apiKey)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ar *apiKeyRepo) DeleteAPIKey(ctx context.Context, id int) (err error) {\n\t_, err = ar.data.DB.Context(ctx).ID(id).Delete(&entity.APIKey{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/auth/auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage auth\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/service/auth\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// authRepo auth repository\ntype authRepo struct {\n\tdata *data.Data\n}\n\n// NewAuthRepo new repository\nfunc NewAuthRepo(data *data.Data) auth.AuthRepo {\n\treturn &authRepo{\n\t\tdata: data,\n\t}\n}\n\n// GetUserCacheInfo get user cache info\nfunc (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {\n\tuserInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil, nil\n\t}\n\tuserInfo = &entity.UserCacheInfo{}\n\t_ = json.Unmarshal([]byte(userInfoCache), userInfo)\n\treturn userInfo, nil\n}\n\n// SetUserCacheInfo set user cache info\nfunc (ar *authRepo) SetUserCacheInfo(ctx context.Context,\n\taccessToken, visitToken string, userInfo *entity.UserCacheInfo) (err error) {\n\tuserInfo.VisitToken = visitToken\n\tuserInfoCache, err := json.Marshal(userInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken,\n\t\tstring(userInfoCache), constant.UserTokenCacheTime)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil {\n\t\tlog.Error(err)\n\t}\n\tif len(visitToken) == 0 {\n\t\treturn nil\n\t}\n\tif err := ar.data.Cache.SetString(ctx, constant.UserVisitTokenCacheKey+visitToken,\n\t\taccessToken, constant.UserTokenCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn nil\n}\n\n// GetUserVisitCacheInfo get user visit cache info\nfunc (ar *authRepo) GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) {\n\taccessToken, exist, err := ar.data.Cache.GetString(ctx, constant.UserVisitTokenCacheKey+visitToken)\n\tif err != nil {\n\t\treturn \"\", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn \"\", nil\n\t}\n\treturn accessToken, nil\n}\n\n// RemoveUserCacheInfo remove user cache info\nfunc (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) {\n\terr = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// RemoveUserVisitCacheInfo remove visit token cache\nfunc (ar *authRepo) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) {\n\terr = ar.data.Cache.Del(ctx, constant.UserVisitTokenCacheKey+visitToken)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// SetUserStatus set user status\nfunc (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) {\n\tuserInfoCache, err := json.Marshal(userInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ar.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID,\n\t\tstring(userInfoCache), constant.UserStatusChangedCacheTime)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// GetUserStatus get user status\nfunc (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) {\n\tuserInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil, nil\n\t}\n\tuserInfo = &entity.UserCacheInfo{}\n\t_ = json.Unmarshal([]byte(userInfoCache), userInfo)\n\treturn userInfo, nil\n}\n\n// RemoveUserStatus remove user status\nfunc (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) {\n\terr = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// GetAdminUserCacheInfo get admin user cache info\nfunc (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {\n\tuserInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, nil\n\t}\n\tuserInfo = &entity.UserCacheInfo{}\n\t_ = json.Unmarshal([]byte(userInfoCache), userInfo)\n\treturn userInfo, nil\n}\n\n// SetAdminUserCacheInfo set admin user cache info\nfunc (ar *authRepo) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {\n\tuserInfoCache, err := json.Marshal(userInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ar.data.Cache.SetString(ctx, constant.AdminTokenCacheKey+accessToken, string(userInfoCache),\n\t\tconstant.AdminTokenCacheTime)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// RemoveAdminUserCacheInfo remove admin user cache info\nfunc (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) {\n\terr = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// AddUserTokenMapping add user token mapping\nfunc (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {\n\tkey := constant.UserTokenMappingCacheKey + userID\n\tresp, _, err := ar.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmapping := make(map[string]bool, 0)\n\tif len(resp) > 0 {\n\t\t_ = json.Unmarshal([]byte(resp), &mapping)\n\t}\n\tmapping[accessToken] = true\n\tcontent, _ := json.Marshal(mapping)\n\treturn ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)\n}\n\n// RemoveUserTokens Log out all users under this user id\nfunc (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {\n\tkey := constant.UserTokenMappingCacheKey + userID\n\tresp, _, err := ar.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\treturn\n\t}\n\tmapping := make(map[string]bool, 0)\n\tif len(resp) > 0 {\n\t\t_ = json.Unmarshal([]byte(resp), &mapping)\n\t\tlog.Debugf(\"find %d user tokens by user id %s\", len(mapping), userID)\n\t}\n\n\tfor token := range mapping {\n\t\tif token == remainToken {\n\t\t\tcontinue\n\t\t}\n\t\tif err := ar.RemoveUserCacheInfo(ctx, token); err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\tlog.Debugf(\"del user %s token success\", userID)\n\t\t}\n\t}\n\tif err := ar.RemoveUserStatus(ctx, userID); err != nil {\n\t\tlog.Error(err)\n\t}\n\tif err := ar.data.Cache.Del(ctx, key); err != nil {\n\t\tlog.Error(err)\n\t}\n}\n"
  },
  {
    "path": "internal/repo/badge/badge_event_rule.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// eventRuleRepo event rule repo\ntype eventRuleRepo struct {\n\tdata             *data.Data\n\tEventRuleMapping map[constant.EventType][]badge.EventRuleHandler\n}\n\n// NewEventRuleRepo creates a new badge repository\nfunc NewEventRuleRepo(data *data.Data) badge.EventRuleRepo {\n\tb := &eventRuleRepo{\n\t\tdata: data,\n\t}\n\tb.EventRuleMapping = map[constant.EventType][]badge.EventRuleHandler{\n\t\tconstant.EventUserUpdate:     {b.FirstUpdateUserProfile},\n\t\tconstant.EventUserShare:      {b.FirstSharedPost},\n\t\tconstant.EventQuestionCreate: nil,\n\t\tconstant.EventQuestionUpdate: {b.FirstPostEdit},\n\t\tconstant.EventQuestionDelete: nil,\n\t\tconstant.EventQuestionVote:   {b.FirstVotedPost, b.ReachQuestionVote},\n\t\tconstant.EventQuestionAccept: {b.FirstAcceptAnswer, b.ReachAnswerAcceptedAmount},\n\t\tconstant.EventQuestionFlag:   {b.FirstFlaggedPost},\n\t\tconstant.EventQuestionReact:  {b.FirstReactedPost},\n\t\tconstant.EventAnswerCreate:   nil,\n\t\tconstant.EventAnswerUpdate:   {b.FirstPostEdit},\n\t\tconstant.EventAnswerDelete:   nil,\n\t\tconstant.EventAnswerVote:     {b.FirstVotedPost, b.ReachAnswerVote},\n\t\tconstant.EventAnswerFlag:     {b.FirstFlaggedPost},\n\t\tconstant.EventAnswerReact:    {b.FirstReactedPost},\n\t\tconstant.EventCommentCreate:  nil,\n\t\tconstant.EventCommentUpdate:  nil,\n\t\tconstant.EventCommentDelete:  nil,\n\t\tconstant.EventCommentVote:    {b.FirstVotedPost},\n\t\tconstant.EventCommentFlag:    {b.FirstFlaggedPost},\n\t}\n\treturn b\n}\n\n// HandleEventWithRule handle event with rule\nfunc (br *eventRuleRepo) HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) (\n\tawards []*entity.BadgeAward) {\n\thandlers := br.EventRuleMapping[msg.EventType]\n\tfor _, handler := range handlers {\n\t\tt, err := handler(ctx, msg)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"error handling badge event %+v: %v\", msg, err)\n\t\t} else {\n\t\t\tawards = append(awards, t...)\n\t\t}\n\t}\n\treturn awards\n}\n\n// FirstUpdateUserProfile first update user profile\nfunc (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstUpdateUserProfile\")\n\tfor _, b := range badges {\n\t\tbean := &entity.User{ID: event.UserID}\n\t\texist, err := br.data.DB.Context(ctx).Get(bean)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif !exist {\n\t\t\tcontinue\n\t\t}\n\t\tif len(bean.Bio) > 0 {\n\t\t\tawards = append(awards, br.createBadgeAward(event.UserID, entity.BadgeEmptyAwardKey, b))\n\t\t}\n\t}\n\treturn awards, nil\n}\n\n// FirstPostEdit first post edit\nfunc (br *eventRuleRepo) FirstPostEdit(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstPostEdit\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// FirstFlaggedPost first flagged post.\nfunc (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstFlaggedPost\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// FirstVotedPost first voted post\nfunc (br *eventRuleRepo) FirstVotedPost(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstVotedPost\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// FirstReactedPost first reacted post\nfunc (br *eventRuleRepo) FirstReactedPost(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstReactedPost\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// FirstSharedPost first shared post\nfunc (br *eventRuleRepo) FirstSharedPost(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstSharedPost\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// FirstAcceptAnswer user first accept answer\nfunc (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"FirstAcceptAnswer\")\n\tfor _, b := range badges {\n\t\tawards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))\n\t}\n\treturn awards, nil\n}\n\n// ReachAnswerAcceptedAmount reach answer accepted amount\nfunc (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"ReachAnswerAcceptedAmount\")\n\tif len(event.AnswerUserID) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// count user's accepted answer amount\n\tamount, err := br.data.DB.Context(ctx).Count(&entity.Answer{\n\t\tUserID:   event.AnswerUserID,\n\t\tAccepted: schema.AnswerAcceptedEnable,\n\t\tStatus:   entity.AnswerStatusAvailable,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\tfor _, b := range badges {\n\t\t// get badge requirement\n\t\trequirement := b.GetIntParam(\"amount\")\n\t\tif requirement == 0 || amount < requirement {\n\t\t\tcontinue\n\t\t}\n\t\tawards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b))\n\t}\n\treturn awards, nil\n}\n\n// ReachAnswerVote reach answer vote\nfunc (br *eventRuleRepo) ReachAnswerVote(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"ReachAnswerVote\")\n\t// get vote amount\n\tamount, _ := strconv.Atoi(event.GetExtra(\"vote_up_amount\"))\n\tif amount == 0 {\n\t\treturn nil, nil\n\t}\n\n\tfor _, b := range badges {\n\t\t// get badge requirement\n\t\trequirement := b.GetIntParam(\"amount\")\n\t\tif requirement == 0 || int64(amount) < requirement {\n\t\t\tcontinue\n\t\t}\n\t\tawards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b))\n\t}\n\treturn awards, nil\n}\n\n// ReachQuestionVote reach question vote\nfunc (br *eventRuleRepo) ReachQuestionVote(ctx context.Context,\n\tevent *schema.EventMsg) (awards []*entity.BadgeAward, err error) {\n\tbadges := br.getBadgesByHandler(ctx, \"ReachQuestionVote\")\n\t// get vote amount\n\tamount, _ := strconv.Atoi(event.GetExtra(\"vote_up_amount\"))\n\tif amount == 0 {\n\t\treturn nil, nil\n\t}\n\n\tfor _, b := range badges {\n\t\t// get badge requirement\n\t\trequirement := b.GetIntParam(\"amount\")\n\t\tif requirement == 0 || int64(amount) < requirement {\n\t\t\tcontinue\n\t\t}\n\t\tawards = append(awards, br.createBadgeAward(event.QuestionUserID, event.QuestionID, b))\n\t}\n\treturn awards, nil\n}\n\nfunc (br *eventRuleRepo) getBadgesByHandler(ctx context.Context, handler string) (badges []*entity.Badge) {\n\tbadges = make([]*entity.Badge, 0)\n\terr := br.data.DB.Context(ctx).Where(\"handler = ?\", handler).Find(&badges)\n\tif err != nil {\n\t\tlog.Errorf(\"error getting badge by handler %s: %v\", handler, err)\n\t\treturn nil\n\t}\n\treturn badges\n}\n\nfunc (br *eventRuleRepo) createBadgeAward(userID, awardKey string, badge *entity.Badge) (awards *entity.BadgeAward) {\n\treturn &entity.BadgeAward{\n\t\tUserID:   userID,\n\t\tBadgeID:  badge.ID,\n\t\tAwardKey: awardKey,\n\t}\n}\n"
  },
  {
    "path": "internal/repo/badge/badge_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\ntype badgeRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewBadgeRepo creates a new badge repository\nfunc NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeRepo {\n\treturn &badgeRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\nfunc (r *badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) {\n\tbadge = &entity.Badge{}\n\texists, err = r.data.DB.Context(ctx).Where(\"id = ?\", id).Get(badge)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (r *badgeRepo) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) {\n\tbadges = make([]*entity.Badge, 0)\n\terr = r.data.DB.Context(ctx).In(\"id\", ids).Find(&badges)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// ListPaged returns a list of activated badges\nfunc (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) {\n\tbadges = make([]*entity.Badge, 0)\n\ttotal = 0\n\n\tsession := r.data.DB.Context(ctx).Where(\"status <> ?\", entity.BadgeStatusDeleted)\n\tif page == 0 || pageSize == 0 {\n\t\terr = session.Find(&badges)\n\t} else {\n\t\ttotal, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session)\n\t}\n\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// ListActivated returns a list of activated badges\nfunc (r *badgeRepo) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) {\n\tbadges = make([]*entity.Badge, 0)\n\ttotal = 0\n\n\tsession := r.data.DB.Context(ctx).Where(\"status = ?\", entity.BadgeStatusActive)\n\tif page == 0 || pageSize == 0 {\n\t\terr = session.Find(&badges)\n\t} else {\n\t\ttotal, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session)\n\t}\n\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// ListInactivated returns a list of inactivated badges\nfunc (r *badgeRepo) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) {\n\tbadges = make([]*entity.Badge, 0)\n\ttotal = 0\n\n\tsession := r.data.DB.Context(ctx).Where(\"status = ?\", entity.BadgeStatusInactive)\n\tif page == 0 || pageSize == 0 {\n\t\terr = session.Find(&badges)\n\t} else {\n\t\ttotal, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session)\n\t}\n\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateStatus updates the award count of a badge\nfunc (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (err error) {\n\t_, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\t_, err = session.ID(id).Update(&entity.Badge{\n\t\t\tStatus: status,\n\t\t})\n\t\tif err != nil {\n\t\t\terr = errors.InternalServer(reason.DatabaseError).WithError(session.Rollback()).WithStack()\n\t\t\treturn\n\t\t}\n\t\tif status >= entity.BadgeStatusDeleted {\n\t\t\t_, err = session.Where(\"badge_id = ?\", id).Cols(\"is_badge_deleted\").Update(&entity.BadgeAward{\n\t\t\t\tIsBadgeDeleted: entity.IsBadgeDeleted,\n\t\t\t})\n\t\t} else {\n\t\t\t_, err = session.Where(\"badge_id = ?\", id).Cols(\"is_badge_deleted\").Update(&entity.BadgeAward{\n\t\t\t\tIsBadgeDeleted: entity.IsBadgeNotDeleted,\n\t\t\t})\n\t\t}\n\t\treturn\n\t})\n\n\treturn\n}\n\n// UpdateAwardCount updates the award count of a badge\nfunc (r *badgeRepo) UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) {\n\t_, err = r.data.DB.Context(ctx).ID(badgeID).Cols(\"award_count\").Update(&entity.Badge{AwardCount: awardCount})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/badge_award/badge_award_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge_award\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\ntype badgeAwardRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\nfunc NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeAwardRepo {\n\treturn &badgeAwardRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AwardBadgeForUser award badge for user\nfunc (r *badgeAwardRepo) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) {\n\tbadgeAward.ID, err = r.uniqueIDRepo.GenUniqueIDStr(ctx, entity.BadgeAward{}.TableName())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\n\t\tbadgeInfo := &entity.Badge{}\n\t\texist, err := session.ID(badgeAward.BadgeID).ForUpdate().Get(badgeInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, fmt.Errorf(\"badge not exist\")\n\t\t}\n\n\t\told := &entity.BadgeAward{\n\t\t\tUserID:         badgeAward.UserID,\n\t\t\tBadgeID:        badgeAward.BadgeID,\n\t\t\tIsBadgeDeleted: entity.IsBadgeNotDeleted,\n\t\t}\n\t\tif badgeInfo.Single != entity.BadgeSingleAward {\n\t\t\told.AwardKey = badgeAward.AwardKey\n\t\t}\n\t\texist, err = session.Get(old)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\treturn nil, fmt.Errorf(\"badge already awarded\")\n\t\t}\n\n\t\t_, err = session.Insert(badgeAward)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn session.ID(badgeInfo.ID).Incr(\"award_count\", 1).Update(&entity.Badge{})\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// CheckIsAward check this badge is awarded for this user or not\nfunc (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awardKey string, singleOrMulti int8) (\n\tisAward bool, err error) {\n\tif singleOrMulti == entity.BadgeSingleAward {\n\t\t_, isAward, err = r.GetByUserIdAndBadgeId(ctx, userID, badgeID)\n\t} else {\n\t\t_, isAward, err = r.GetByUserIdAndBadgeIdAndAwardKey(ctx, userID, badgeID, awardKey)\n\t}\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn isAward, err\n}\n\nfunc (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) {\n\tawardCount, err := r.data.DB.Context(ctx).Where(\"user_id = ? AND badge_id = ?\", userID, badgeID).Count(&entity.BadgeAward{})\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn\n}\n\nfunc (r *badgeAwardRepo) CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) {\n\tawardCount, err = r.data.DB.Context(ctx).Count(&entity.BadgeAward{BadgeID: badgeID})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) {\n\terr = r.data.DB.Context(ctx).Select(\"badge_id, count(`id`) AS earned_count\").Where(\"user_id = ?\", userID).GroupBy(\"badge_id\").Find(&earnedCounts)\n\treturn\n}\n\n// ListPagedByBadgeId list badge awards by badge id\nfunc (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) {\n\tsession := r.data.DB.Context(ctx)\n\tsession.Where(\"badge_id = ?\", badgeID)\n\ttotal, err = pager.Help(page, pageSize, &badgeAwardList, &entity.BadgeAward{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// ListPagedByBadgeIdAndUserId list badge awards by badge id and user id\nfunc (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) {\n\tsession := r.data.DB.Context(ctx)\n\tsession.Where(\"badge_id = ? AND user_id = ?\", badgeID, userID)\n\ttotal, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// ListNewestEarned list newest earned badge awards\nfunc (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) {\n\tbadgeAwards = make([]*entity.BadgeAwardRecent, 0)\n\terr = r.data.DB.Context(ctx).\n\t\tSelect(\"badge_id, max(created_at) created,count(*) earned_count\").\n\t\tWhere(\"user_id = ? AND is_badge_deleted = ? \", userID, entity.IsBadgeNotDeleted).\n\t\tGroupBy(\"badge_id\").\n\t\tOrderBy(\"created desc\").\n\t\tLimit(limit).Find(&badgeAwards)\n\treturn\n}\n\n// GetByUserIdAndBadgeId get badge award by user id and badge id\nfunc (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (\n\tbadgeAward *entity.BadgeAward, exists bool, err error) {\n\tbadgeAward = &entity.BadgeAward{}\n\texists, err = r.data.DB.Context(ctx).\n\t\tWhere(\"user_id = ? AND badge_id = ? AND is_badge_deleted = 0\", userID, badgeID).Get(badgeAward)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetByUserIdAndBadgeIdAndAwardKey get badge award by user id and badge id and award key\nfunc (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (\n\tbadgeAward *entity.BadgeAward, exists bool, err error) {\n\tbadgeAward = &entity.BadgeAward{}\n\texists, err = r.data.DB.Context(ctx).\n\t\tWhere(\"user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0\", userID, badgeID, awardKey).Get(badgeAward)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// DeleteUserBadgeAward delete user badge award\nfunc (r *badgeAwardRepo) DeleteUserBadgeAward(ctx context.Context, userID string) (err error) {\n\t_, err = r.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Delete(&entity.BadgeAward{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/badge_group/badge_group_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge_group\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/internal/service/unique\"\n)\n\ntype badgeGroupRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\nfunc NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeGroupRepo {\n\treturn &badgeGroupRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\nfunc (r *badgeGroupRepo) ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) {\n\tgroups = make([]*entity.BadgeGroup, 0)\n\terr = r.data.DB.Context(ctx).Find(&groups)\n\treturn\n}\n\nfunc (r *badgeGroupRepo) AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) {\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/captcha/captcha.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage captcha\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// captchaRepo captcha repository\ntype captchaRepo struct {\n\tdata *data.Data\n}\n\n// NewCaptchaRepo new repository\nfunc NewCaptchaRepo(data *data.Data) action.CaptchaRepo {\n\treturn &captchaRepo{\n\t\tdata: data,\n\t}\n}\n\nfunc (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) {\n\tnow := time.Now()\n\tcacheKey := fmt.Sprintf(\"ActionRecord:%s@%s@%s\", unit, actionType, now.Format(\"2006-1-02\"))\n\tvalue := &entity.ActionRecordInfo{}\n\tvalue.LastTime = now.Unix()\n\tvalue.Num = amount\n\tvalueStr, err := json.Marshal(value)\n\tif err != nil {\n\t\treturn nil\n\t}\n\terr = cr.data.Cache.SetString(ctx, cacheKey, string(valueStr), 6*time.Minute)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actionInfo *entity.ActionRecordInfo, err error) {\n\tnow := time.Now()\n\tcacheKey := fmt.Sprintf(\"ActionRecord:%s@%s@%s\", unit, actionType, now.Format(\"2006-1-02\"))\n\tres, exist, err := cr.data.Cache.GetString(ctx, cacheKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, nil\n\t}\n\tactionInfo = &entity.ActionRecordInfo{}\n\t_ = json.Unmarshal([]byte(res), actionInfo)\n\treturn actionInfo, nil\n}\n\nfunc (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) {\n\tnow := time.Now()\n\tcacheKey := fmt.Sprintf(\"ActionRecord:%s@%s@%s\", unit, actionType, now.Format(\"2006-1-02\"))\n\terr = cr.data.Cache.Del(ctx, cacheKey)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// SetCaptcha set captcha to cache\nfunc (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err error) {\n\terr = cr.data.Cache.SetString(ctx, key, captcha, 6*time.Minute)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCaptcha get captcha from cache\nfunc (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) {\n\tcaptcha, exist, err := cr.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", fmt.Errorf(\"captcha not exist\")\n\t}\n\treturn captcha, nil\n}\n\nfunc (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) {\n\terr = cr.data.Cache.Del(ctx, key)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/repo/collection/collection_group_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage collection\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/service/collection\"\n\t\"xorm.io/xorm\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// collectionGroupRepo collectionGroup repository\ntype collectionGroupRepo struct {\n\tdata *data.Data\n}\n\n// NewCollectionGroupRepo new repository\nfunc NewCollectionGroupRepo(data *data.Data) collection.CollectionGroupRepo {\n\treturn &collectionGroupRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddCollectionGroup add collection group\nfunc (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) {\n\t_, err = cr.data.DB.Context(ctx).Insert(collectionGroup)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// AddCollectionDefaultGroup add collection group\nfunc (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) {\n\tdefaultGroup := &entity.CollectionGroup{\n\t\tName:         \"default\",\n\t\tDefaultGroup: schema.CGDefault,\n\t\tUserID:       userID,\n\t}\n\t_, err = cr.data.DB.Context(ctx).Insert(defaultGroup)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tcollectionGroup = defaultGroup\n\treturn\n}\n\n// CreateDefaultGroupIfNotExist create default group if not exist\nfunc (cr *collectionGroupRepo) CreateDefaultGroupIfNotExist(ctx context.Context, userID string) (\n\tcollectionGroup *entity.CollectionGroup, err error) {\n\t_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\told := &entity.CollectionGroup{\n\t\t\tUserID:       userID,\n\t\t\tDefaultGroup: schema.CGDefault,\n\t\t}\n\t\texist, err := session.ForUpdate().Get(old)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tcollectionGroup = old\n\t\t\treturn old, nil\n\t\t}\n\n\t\tdefaultGroup := &entity.CollectionGroup{\n\t\t\tName:         \"default\",\n\t\t\tDefaultGroup: schema.CGDefault,\n\t\t\tUserID:       userID,\n\t\t}\n\t\t_, err = session.Insert(defaultGroup)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcollectionGroup = defaultGroup\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn collectionGroup, nil\n}\n\n// UpdateCollectionGroup update collection group\nfunc (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) {\n\t_, err = cr.data.DB.Context(ctx).ID(collectionGroup.ID).Cols(cols...).Update(collectionGroup)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCollectionGroup get collection group one\nfunc (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string) (\n\tcollectionGroup *entity.CollectionGroup, exist bool, err error,\n) {\n\tcollectionGroup = &entity.CollectionGroup{}\n\texist, err = cr.data.DB.Context(ctx).ID(id).Get(collectionGroup)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCollectionGroupPage get collection group page\nfunc (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) {\n\tcollectionGroupList = make([]*entity.CollectionGroup, 0)\n\n\tsession := cr.data.DB.Context(ctx)\n\tif collectionGroup.UserID != \"\" && collectionGroup.UserID != \"0\" {\n\t\tsession = session.Where(\"user_id = ?\", collectionGroup.UserID)\n\t}\n\tsession = session.OrderBy(\"update_time desc\")\n\n\ttotal, err = pager.Help(page, pageSize, collectionGroupList, collectionGroup, session)\n\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\treturn\n}\n\nfunc (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) {\n\tcollectionGroup = &entity.CollectionGroup{}\n\thas, err = cr.data.DB.Context(ctx).Where(\"user_id =? and  default_group = ?\", userID, schema.CGDefault).Get(collectionGroup)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/collection/collection_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage collection\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// collectionRepo collection repository\ntype collectionRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewCollectionRepo new repository\nfunc NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collectioncommon.CollectionRepo {\n\treturn &collectionRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddCollection add collection\nfunc (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {\n\tcollection.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\told := &entity.Collection{\n\t\t\tUserID:   collection.UserID,\n\t\t\tObjectID: collection.ObjectID,\n\t\t}\n\t\texist, err := session.ForUpdate().Get(old)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\treturn nil, nil\n\t\t}\n\t\t_, err = session.Insert(collection)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// RemoveCollection delete collection\nfunc (cr *collectionRepo) RemoveCollection(ctx context.Context, id string) (err error) {\n\t_, err = cr.data.DB.Context(ctx).Where(\"id = ?\", id).Delete(&entity.Collection{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// UpdateCollection update collection\nfunc (cr *collectionRepo) UpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error) {\n\t_, err = cr.data.DB.Context(ctx).ID(collection.ID).Cols(cols...).Update(collection)\n\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n}\n\n// GetCollection get collection one\nfunc (cr *collectionRepo) GetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error) {\n\tcollection = &entity.Collection{}\n\texist, err = cr.data.DB.Context(ctx).ID(id).Get(collection)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCollectionList get collection list all\nfunc (cr *collectionRepo) GetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error) {\n\tcollectionList = make([]*entity.Collection, 0)\n\terr = cr.data.DB.Context(ctx).Find(&collectionList, collection)\n\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\treturn\n}\n\n// GetOneByObjectIDAndUser get one by object TagID and user\nfunc (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userID string, objectID string) (collection *entity.Collection, exist bool, err error) {\n\tcollection = &entity.Collection{}\n\texist, err = cr.data.DB.Context(ctx).Where(\"user_id = ? and object_id = ?\", userID, objectID).Get(collection)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// SearchByObjectIDsAndUser search by object IDs and user\nfunc (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userID string, objectIDs []string) ([]*entity.Collection, error) {\n\tcollectionList := make([]*entity.Collection, 0)\n\terr := cr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).In(\"object_id\", objectIDs).Find(&collectionList)\n\tif err != nil {\n\t\treturn collectionList, err\n\t}\n\treturn collectionList, nil\n}\n\n// CountByObjectID count by object TagID\nfunc (cr *collectionRepo) CountByObjectID(ctx context.Context, objectID string) (total int64, err error) {\n\tcollection := &entity.Collection{}\n\ttotal, err = cr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).Count(collection)\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCollectionPage get collection page\nfunc (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) {\n\tcollectionList = make([]*entity.Collection, 0)\n\n\tsession := cr.data.DB.Context(ctx)\n\tif collection.UserID != \"\" && collection.UserID != \"0\" {\n\t\tsession = session.Where(\"user_id = ?\", collection.UserID)\n\t}\n\n\tif collection.UserCollectionGroupID != \"\" && collection.UserCollectionGroupID != \"0\" {\n\t\tsession = session.Where(\"user_collection_group_id = ?\", collection.UserCollectionGroupID)\n\t}\n\tsession = session.OrderBy(\"update_time desc\")\n\n\ttotal, err = pager.Help(page, pageSize, collectionList, collection, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// SearchObjectCollected check object is collected or not\nfunc (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) {\n\tfor i := range objectIds {\n\t\tobjectIds[i] = uid.DeShortID(objectIds[i])\n\t}\n\n\tlist, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\tcollectedMap := make(map[string]bool)\n\tshort := handler.GetEnableShortID(ctx)\n\tfor _, item := range list {\n\t\tif short {\n\t\t\titem.ObjectID = uid.EnShortID(item.ObjectID)\n\t\t}\n\t\tcollectedMap[item.ObjectID] = true\n\t}\n\treturn collectedMap, nil\n}\n\n// SearchList\nfunc (cr *collectionRepo) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) {\n\tvar count int64\n\tvar err error\n\trows := make([]*entity.Collection, 0)\n\tif search.Page > 0 {\n\t\tsearch.Page--\n\t} else {\n\t\tsearch.Page = 0\n\t}\n\tif search.PageSize == 0 {\n\t\tsearch.PageSize = constant.DefaultPageSize\n\t}\n\toffset := search.Page * search.PageSize\n\tsession := cr.data.DB.Context(ctx).Where(\"\")\n\tif len(search.UserID) > 0 {\n\t\tsession = session.And(\"user_id = ?\", search.UserID)\n\t} else {\n\t\treturn rows, count, nil\n\t}\n\tsession = session.Limit(search.PageSize, offset)\n\tcount, err = session.OrderBy(\"updated_at desc\").FindAndCount(&rows)\n\tif err != nil {\n\t\treturn rows, count, err\n\t}\n\treturn rows, count, nil\n}\n"
  },
  {
    "path": "internal/repo/comment/comment_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage comment\n\nimport (\n\t\"context\"\n\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// commentRepo comment repository\ntype commentRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewCommentRepo new repository\nfunc NewCommentRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment.CommentRepo {\n\treturn &commentRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// NewCommentCommonRepo new repository\nfunc NewCommentCommonRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment_common.CommentCommonRepo {\n\treturn &commentRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddComment add comment\nfunc (cr *commentRepo) AddComment(ctx context.Context, comment *entity.Comment) (err error) {\n\tcomment.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, comment.TableName())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = cr.data.DB.Context(ctx).Insert(comment)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RemoveComment delete comment\nfunc (cr *commentRepo) RemoveComment(ctx context.Context, commentID string) (err error) {\n\tsession := cr.data.DB.Context(ctx).ID(commentID)\n\t_, err = session.Update(&entity.Comment{Status: entity.CommentStatusDeleted})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateCommentContent update comment\nfunc (cr *commentRepo) UpdateCommentContent(\n\tctx context.Context, commentID string, originalText string, parsedText string) (err error) {\n\t_, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{\n\t\tOriginalText: originalText,\n\t\tParsedText:   parsedText,\n\t})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateCommentStatus update comment status\nfunc (cr *commentRepo) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) {\n\t_, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{\n\t\tStatus: status,\n\t})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetComment get comment one\nfunc (cr *commentRepo) GetComment(ctx context.Context, commentID string) (\n\tcomment *entity.Comment, exist bool, err error) {\n\tcomment = &entity.Comment{}\n\texist, err = cr.data.DB.Context(ctx).Where(\"status = ?\", entity.CommentStatusAvailable).ID(commentID).Get(comment)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCommentWithoutStatus get comment one without status\nfunc (cr *commentRepo) GetCommentWithoutStatus(ctx context.Context, commentID string) (\n\tcomment *entity.Comment, exist bool, err error) {\n\tcomment = &entity.Comment{}\n\texist, err = cr.data.DB.Context(ctx).ID(commentID).Get(comment)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) {\n\tlist := make([]*entity.Comment, 0)\n\tcount, err = cr.data.DB.Context(ctx).Where(\"status = ?\", entity.CommentStatusAvailable).FindAndCount(&list)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetCommentPage get comment page\nfunc (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (\n\tcommentList []*entity.Comment, total int64, err error,\n) {\n\tcommentList = make([]*entity.Comment, 0)\n\n\tsession := cr.data.DB.Context(ctx)\n\tsession.OrderBy(commentQuery.GetOrderBy())\n\tsession.Where(\"status = ?\", entity.CommentStatusAvailable)\n\n\tcond := &entity.Comment{ObjectID: commentQuery.ObjectID, UserID: commentQuery.UserID}\n\ttotal, err = pager.Help(commentQuery.Page, commentQuery.PageSize, &commentList, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RemoveAllUserComment remove all user comment\nfunc (cr *commentRepo) RemoveAllUserComment(ctx context.Context, userID string) (err error) {\n\tsession := cr.data.DB.Context(ctx).Where(\"user_id = ?\", userID)\n\tsession.Where(\"status != ?\", entity.CommentStatusDeleted)\n\taffected, err := session.Update(&entity.Comment{Status: entity.CommentStatusDeleted})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tlog.Infof(\"delete user comment, userID: %s, affected: %d\", userID, affected)\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/config/config_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage config\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// configRepo config repository\ntype configRepo struct {\n\tdata *data.Data\n}\n\n// NewConfigRepo new repository\nfunc NewConfigRepo(data *data.Data) config.ConfigRepo {\n\trepo := &configRepo{\n\t\tdata: data,\n\t}\n\treturn repo\n}\n\nfunc (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) {\n\tcacheKey := fmt.Sprintf(\"%s%d\", constant.ConfigID2KEYCacheKeyPrefix, id)\n\tcacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey)\n\tif err == nil && exist && len(cacheData) > 0 {\n\t\tc = &entity.Config{}\n\t\tc.BuildByJSON([]byte(cacheData))\n\t\tif c.ID > 0 {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\tc = &entity.Config{}\n\texist, err = cr.data.DB.Context(ctx).ID(id).Get(c)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil, fmt.Errorf(\"config not found by id: %d\", id)\n\t}\n\n\t// update cache\n\tif err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn c, nil\n}\n\nfunc (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) {\n\tcacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key\n\tcacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey)\n\tif err == nil && exist && len(cacheData) > 0 {\n\t\tc = &entity.Config{}\n\t\tc.BuildByJSON([]byte(cacheData))\n\t\tif c.ID > 0 {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\tc = &entity.Config{Key: key}\n\texist, err = cr.data.DB.Context(ctx).Get(c)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil, fmt.Errorf(\"config not found by key: %s\", key)\n\t}\n\n\t// update cache\n\tif err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn c, nil\n}\n\nfunc (cr configRepo) GetConfigByKeyFromDB(ctx context.Context, key string) (c *entity.Config, err error) {\n\tc = &entity.Config{Key: key}\n\texist, err := cr.data.DB.Context(ctx).Get(c)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil, fmt.Errorf(\"config not found by key: %s\", key)\n\t}\n\treturn c, nil\n}\n\nfunc (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) {\n\t// check if key exists\n\toldConfig := &entity.Config{Key: key}\n\texist, err := cr.data.DB.Context(ctx).Get(oldConfig)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\t// update database\n\t_, err = cr.data.DB.Context(ctx).ID(oldConfig.ID).Update(&entity.Config{Value: value})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\toldConfig.Value = value\n\tcacheVal := oldConfig.JsonString()\n\t// update cache\n\tif err := cr.data.Cache.SetString(ctx,\n\t\tconstant.ConfigKEY2ContentCacheKeyPrefix+key, cacheVal, constant.ConfigCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\tif err := cr.data.Cache.SetString(ctx,\n\t\tfmt.Sprintf(\"%s%d\", constant.ConfigID2KEYCacheKeyPrefix, oldConfig.ID), cacheVal, constant.ConfigCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/export/email_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage export\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/tidwall/gjson\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// emailRepo email repository\ntype emailRepo struct {\n\tdata *data.Data\n}\n\n// NewEmailRepo new repository\nfunc NewEmailRepo(data *data.Data) export.EmailRepo {\n\treturn &emailRepo{\n\t\tdata: data,\n\t}\n}\n\n// SetCode The email code is used to verify that the link in the message is out of date\nfunc (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error {\n\t// Setting the latest code is to help ensure that only one link is active at a time.\n\t// Set userID -> latest code\n\tif err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// Set latest code -> content\n\tif err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// VerifyCode verify the code if out of date\nfunc (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) {\n\t// Get latest code -> content\n\tcodeCacheKey := constant.UserEmailCodeCacheKey + code\n\tcontent, exist, err := e.data.Cache.GetString(ctx, codeCacheKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", nil\n\t}\n\n\t// Delete the code after verification\n\t_ = e.data.Cache.Del(ctx, codeCacheKey)\n\n\t// If some email content does not need to verify the latest code is the same as the code, skip it.\n\t// For example, some unsubscribe email content does not need to verify the latest code.\n\t// This link always works before the code is out of date.\n\tif skipValidationLatestCode := gjson.Get(content, \"skip_validation_latest_code\").Bool(); skipValidationLatestCode {\n\t\treturn content, nil\n\t}\n\tuserID := gjson.Get(content, \"user_id\").String()\n\n\t// Get userID -> latest code\n\tlatestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", nil\n\t}\n\n\t// Check if the latest code is the same as the code, if not, means the code is out of date\n\tif latestCode != code {\n\t\treturn \"\", nil\n\t}\n\treturn content, nil\n}\n"
  },
  {
    "path": "internal/repo/file_record/file_record_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage file_record\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/service/file_record\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// fileRecordRepo fileRecord repository\ntype fileRecordRepo struct {\n\tdata *data.Data\n}\n\n// NewFileRecordRepo new repository\nfunc NewFileRecordRepo(data *data.Data) file_record.FileRecordRepo {\n\treturn &fileRecordRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddFileRecord add file record\nfunc (fr *fileRecordRepo) AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {\n\t_, err = fr.data.DB.Context(ctx).Insert(fileRecord)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetFileRecordPage get fileRecord page\nfunc (fr *fileRecordRepo) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) (\n\tfileRecordList []*entity.FileRecord, total int64, err error) {\n\tfileRecordList = make([]*entity.FileRecord, 0)\n\n\tsession := fr.data.DB.Context(ctx)\n\ttotal, err = pager.Help(page, pageSize, &fileRecordList, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// DeleteFileRecord delete file record\nfunc (fr *fileRecordRepo) DeleteFileRecord(ctx context.Context, id int) (err error) {\n\t_, err = fr.data.DB.Context(ctx).ID(id).Cols(\"status\").Update(&entity.FileRecord{Status: entity.FileRecordStatusDeleted})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateFileRecord update file record\nfunc (fr *fileRecordRepo) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {\n\t_, err = fr.data.DB.Context(ctx).ID(fileRecord.ID).Update(fileRecord)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetFileRecordByURL gets a file record by its url\nfunc (fr *fileRecordRepo) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) {\n\trecord = &entity.FileRecord{}\n\tsession := fr.data.DB.Context(ctx)\n\texists, err := session.Where(\"file_url = ? AND status = ?\", fileURL, entity.FileRecordStatusAvailable).Get(record)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif !exists {\n\t\treturn\n\t}\n\treturn record, nil\n}\n"
  },
  {
    "path": "internal/repo/limit/limit.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage limit\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// LimitRepo auth repository\ntype LimitRepo struct {\n\tdata *data.Data\n}\n\n// NewRateLimitRepo new repository\nfunc NewRateLimitRepo(data *data.Data) *LimitRepo {\n\treturn &LimitRepo{\n\t\tdata: data,\n\t}\n}\n\n// CheckAndRecord check\nfunc (lr *LimitRepo) CheckAndRecord(ctx context.Context, key string) (limit bool, err error) {\n\t_, exist, err := lr.data.Cache.GetString(ctx, constant.RateLimitCacheKeyPrefix+key)\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif exist {\n\t\treturn true, nil\n\t}\n\terr = lr.data.Cache.SetString(ctx, constant.RateLimitCacheKeyPrefix+key,\n\t\tfmt.Sprintf(\"%d\", time.Now().Unix()), constant.RateLimitCacheTime)\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn false, nil\n}\n\n// ClearRecord clear\nfunc (lr *LimitRepo) ClearRecord(ctx context.Context, key string) error {\n\treturn lr.data.Cache.Del(ctx, constant.RateLimitCacheKeyPrefix+key)\n}\n"
  },
  {
    "path": "internal/repo/meta/meta_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage meta\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// metaRepo meta repository\ntype metaRepo struct {\n\tdata *data.Data\n}\n\n// NewMetaRepo new repository\nfunc NewMetaRepo(data *data.Data) metacommon.MetaRepo {\n\treturn &metaRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddMeta add meta\nfunc (mr *metaRepo) AddMeta(ctx context.Context, meta *entity.Meta) (err error) {\n\t_, err = mr.data.DB.Context(ctx).Insert(meta)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RemoveMeta delete meta\nfunc (mr *metaRepo) RemoveMeta(ctx context.Context, id int) (err error) {\n\t_, err = mr.data.DB.Context(ctx).ID(id).Delete(&entity.Meta{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateMeta update meta\nfunc (mr *metaRepo) UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) {\n\t_, err = mr.data.DB.Context(ctx).ID(meta.ID).Update(meta)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// AddOrUpdateMetaByObjectIdAndKey if exist record with same objectID and key, update it. Or create a new one\nfunc (mr *metaRepo) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error {\n\t_, err := mr.data.DB.Transaction(func(session *xorm.Session) (any, error) {\n\t\tsession = session.Context(ctx)\n\n\t\t// 1. acquire meta entity with target object id and key\n\t\tmetaEntity := &entity.Meta{}\n\t\texist, err := session.Where(builder.Eq{\"object_id\": objectId}.And(builder.Eq{\"`key`\": key})).ForUpdate().Get(metaEntity)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmeta, err := f(metaEntity, exist)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// return entity.Meta\n\t\tif exist {\n\t\t\t_, err = session.ID(metaEntity.ID).Update(meta)\n\t\t} else {\n\t\t\t_, err = session.Insert(meta)\n\t\t}\n\n\t\treturn nil, err\n\t})\n\treturn err\n}\n\n// GetMetaByObjectIdAndKey get meta one\nfunc (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (\n\tmeta *entity.Meta, exist bool, err error) {\n\tmeta = &entity.Meta{}\n\texist, err = mr.data.DB.Context(ctx).Where(builder.Eq{\"object_id\": objectID}.And(builder.Eq{\"`key`\": key})).Desc(\"created_at\").Get(meta)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetMetaList get meta list all\nfunc (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaList []*entity.Meta, err error) {\n\tmetaList = make([]*entity.Meta, 0)\n\terr = mr.data.DB.Context(ctx).Find(&metaList, meta)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/notification/notification_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tnotficationcommon \"github.com/apache/answer/internal/service/notification_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// notificationRepo notification repository\ntype notificationRepo struct {\n\tdata *data.Data\n}\n\n// NewNotificationRepo new repository\nfunc NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo {\n\treturn &notificationRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddNotification add notification\nfunc (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) {\n\tnotification.ObjectID = uid.DeShortID(notification.ObjectID)\n\t_, err = nr.data.DB.Context(ctx).Insert(notification)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) {\n\tnow := time.Now()\n\tnotification.UpdatedAt = now\n\tnotification.ObjectID = uid.DeShortID(notification.ObjectID)\n\t_, err = nr.data.DB.Context(ctx).Where(\"id =?\", notification.ID).Cols(\"content\", \"updated_at\").Update(notification)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) {\n\tinfo := &entity.Notification{}\n\tinfo.IsRead = schema.NotificationRead\n\t_, err = nr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).And(\"type = ?\", notificationType).Cols(\"is_read\").Update(info)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) {\n\tinfo := &entity.Notification{}\n\tinfo.IsRead = schema.NotificationRead\n\t_, err = nr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).And(\"id = ?\", id).Cols(\"is_read\").Update(info)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) {\n\tinfo := &entity.Notification{}\n\texist, err := nr.data.DB.Context(ctx).Where(\"id = ? \", id).Get(info)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn info, false, err\n\t}\n\treturn info, exist, nil\n}\n\nfunc (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) {\n\tinfo := &entity.Notification{}\n\texist, err := nr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).And(\"object_id = ?\", objectID).And(\"type = ?\", notificationType).Get(info)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn info, false, err\n\t}\n\treturn info, exist, nil\n}\n\nfunc (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) (\n\tnotificationList []*entity.Notification, total int64, err error) {\n\tnotificationList = make([]*entity.Notification, 0)\n\tif searchCond.UserID == \"\" {\n\t\treturn notificationList, 0, nil\n\t}\n\n\tsession := nr.data.DB.Context(ctx)\n\tsession = session.Desc(\"updated_at\")\n\n\tcond := &entity.Notification{\n\t\tUserID: searchCond.UserID,\n\t\tType:   searchCond.Type,\n\t}\n\tif searchCond.InboxType > 0 {\n\t\tcond.MsgType = searchCond.InboxType\n\t}\n\ttotal, err = pager.Help(searchCond.Page, searchCond.PageSize, &notificationList, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) {\n\tcount, err := nr.data.DB.Context(ctx).Count(cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, err\n}\n\nfunc (nr *notificationRepo) DeleteNotification(ctx context.Context, userID string) (err error) {\n\t_, err = nr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Delete(&entity.Notification{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (nr *notificationRepo) DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) {\n\t_, err = nr.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Delete(&entity.UserNotificationConfig{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/plugin_config/plugin_config_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin_config\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\ntype pluginConfigRepo struct {\n\tdata *data.Data\n}\n\n// NewPluginConfigRepo new repository\nfunc NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo {\n\treturn &pluginConfigRepo{\n\t\tdata: data,\n\t}\n}\n\nfunc (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) {\n\told := &entity.PluginConfig{PluginSlugName: pluginSlugName}\n\texist, err := ur.data.DB.Context(ctx).Get(old)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif exist {\n\t\told.Value = configValue\n\t\t_, err = ur.data.DB.Context(ctx).ID(old.ID).Update(old)\n\t} else {\n\t\t_, err = ur.data.DB.Context(ctx).Insert(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})\n\t}\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) {\n\tpluginConfigs = make([]*entity.PluginConfig, 0)\n\terr = ur.data.DB.Context(ctx).Find(&pluginConfigs)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn pluginConfigs, err\n}\n"
  },
  {
    "path": "internal/repo/plugin_config/plugin_user_config_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin_config\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"xorm.io/xorm\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\ntype pluginUserConfigRepo struct {\n\tdata *data.Data\n}\n\n// NewPluginUserConfigRepo new repository\nfunc NewPluginUserConfigRepo(data *data.Data) plugin_common.PluginUserConfigRepo {\n\treturn &pluginUserConfigRepo{\n\t\tdata: data,\n\t}\n}\n\nfunc (ur *pluginUserConfigRepo) SaveUserPluginConfig(ctx context.Context, userID string,\n\tpluginSlugName, configValue string) (err error) {\n\t_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {\n\t\tsession = session.Context(ctx)\n\t\told := &entity.PluginUserConfig{\n\t\t\tUserID:         userID,\n\t\t\tPluginSlugName: pluginSlugName,\n\t\t}\n\t\texist, err := session.Get(old)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\told.Value = configValue\n\t\t\t_, err = session.ID(old.ID).Update(old)\n\t\t} else {\n\t\t\t_, err = session.Insert(&entity.PluginUserConfig{\n\t\t\t\tUserID:         userID,\n\t\t\t\tPluginSlugName: pluginSlugName,\n\t\t\t\tValue:          configValue,\n\t\t\t})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *pluginUserConfigRepo) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) (\n\tpluginUserConfig *entity.PluginUserConfig, exist bool, err error) {\n\tpluginUserConfig = &entity.PluginUserConfig{\n\t\tUserID:         userID,\n\t\tPluginSlugName: pluginSlugName,\n\t}\n\texist, err = ur.data.DB.Context(ctx).Get(pluginUserConfig)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn pluginUserConfig, exist, err\n}\n\nfunc (ur *pluginUserConfigRepo) GetPluginUserConfigPage(ctx context.Context, page, pageSize int) (\n\tpluginUserConfigs []*entity.PluginUserConfig, total int64, err error) {\n\tpluginUserConfigs = make([]*entity.PluginUserConfig, 0)\n\ttotal, err = pager.Help(page, pageSize, &pluginUserConfigs, &entity.PluginUserConfig{}, ur.data.DB.Context(ctx))\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ur *pluginUserConfigRepo) DeleteUserPluginConfig(ctx context.Context, userID string) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Delete(&entity.PluginUserConfig{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo\n\nimport (\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/repo/activity\"\n\t\"github.com/apache/answer/internal/repo/activity_common\"\n\t\"github.com/apache/answer/internal/repo/ai_conversation\"\n\t\"github.com/apache/answer/internal/repo/answer\"\n\t\"github.com/apache/answer/internal/repo/api_key\"\n\t\"github.com/apache/answer/internal/repo/auth\"\n\t\"github.com/apache/answer/internal/repo/badge\"\n\t\"github.com/apache/answer/internal/repo/badge_award\"\n\t\"github.com/apache/answer/internal/repo/badge_group\"\n\t\"github.com/apache/answer/internal/repo/captcha\"\n\t\"github.com/apache/answer/internal/repo/collection\"\n\t\"github.com/apache/answer/internal/repo/comment\"\n\t\"github.com/apache/answer/internal/repo/config\"\n\t\"github.com/apache/answer/internal/repo/export\"\n\t\"github.com/apache/answer/internal/repo/file_record\"\n\t\"github.com/apache/answer/internal/repo/limit\"\n\t\"github.com/apache/answer/internal/repo/meta\"\n\t\"github.com/apache/answer/internal/repo/notification\"\n\t\"github.com/apache/answer/internal/repo/plugin_config\"\n\t\"github.com/apache/answer/internal/repo/question\"\n\t\"github.com/apache/answer/internal/repo/rank\"\n\t\"github.com/apache/answer/internal/repo/reason\"\n\t\"github.com/apache/answer/internal/repo/report\"\n\t\"github.com/apache/answer/internal/repo/review\"\n\t\"github.com/apache/answer/internal/repo/revision\"\n\t\"github.com/apache/answer/internal/repo/role\"\n\t\"github.com/apache/answer/internal/repo/search_common\"\n\t\"github.com/apache/answer/internal/repo/site_info\"\n\t\"github.com/apache/answer/internal/repo/tag\"\n\t\"github.com/apache/answer/internal/repo/tag_common\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\t\"github.com/apache/answer/internal/repo/user_external_login\"\n\t\"github.com/apache/answer/internal/repo/user_notification_config\"\n\t\"github.com/google/wire\"\n)\n\n// ProviderSetRepo is data providers.\nvar ProviderSetRepo = wire.NewSet(\n\tdata.NewData,\n\tdata.NewDB,\n\tdata.NewCache,\n\tcomment.NewCommentRepo,\n\tcomment.NewCommentCommonRepo,\n\tcaptcha.NewCaptchaRepo,\n\tunique.NewUniqueIDRepo,\n\treport.NewReportRepo,\n\tactivity_common.NewFollowRepo,\n\tactivity_common.NewVoteRepo,\n\tconfig.NewConfigRepo,\n\tuser.NewUserRepo,\n\tuser.NewUserAdminRepo,\n\trank.NewUserRankRepo,\n\tquestion.NewQuestionRepo,\n\tanswer.NewAnswerRepo,\n\tactivity_common.NewActivityRepo,\n\tactivity.NewVoteRepo,\n\tactivity.NewFollowRepo,\n\tactivity.NewAnswerActivityRepo,\n\tactivity.NewUserActiveActivityRepo,\n\tactivity.NewActivityRepo,\n\tactivity.NewReviewActivityRepo,\n\ttag.NewTagRepo,\n\ttag_common.NewTagCommonRepo,\n\ttag.NewTagRelRepo,\n\tcollection.NewCollectionRepo,\n\tcollection.NewCollectionGroupRepo,\n\tauth.NewAuthRepo,\n\trevision.NewRevisionRepo,\n\tsearch_common.NewSearchRepo,\n\tmeta.NewMetaRepo,\n\texport.NewEmailRepo,\n\treason.NewReasonRepo,\n\tsite_info.NewSiteInfo,\n\tnotification.NewNotificationRepo,\n\trole.NewRoleRepo,\n\trole.NewUserRoleRelRepo,\n\trole.NewRolePowerRelRepo,\n\trole.NewPowerRepo,\n\tuser_external_login.NewUserExternalLoginRepo,\n\tplugin_config.NewPluginConfigRepo,\n\tuser_notification_config.NewUserNotificationConfigRepo,\n\tlimit.NewRateLimitRepo,\n\tplugin_config.NewPluginUserConfigRepo,\n\treview.NewReviewRepo,\n\tbadge.NewBadgeRepo,\n\tbadge.NewEventRuleRepo,\n\tbadge_group.NewBadgeGroupRepo,\n\tbadge_award.NewBadgeAwardRepo,\n\tfile_record.NewFileRecordRepo,\n\tapi_key.NewAPIKeyRepo,\n\tai_conversation.NewAIConversationRepo,\n)\n"
  },
  {
    "path": "internal/repo/question/question_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage question\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// questionRepo question repository\ntype questionRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewQuestionRepo new repository\nfunc NewQuestionRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n) questioncommon.QuestionRepo {\n\treturn &questionRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddQuestion add question\nfunc (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Question) (err error) {\n\tquestion.ID, err = qr.uniqueIDRepo.GenUniqueIDStr(ctx, question.TableName())\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_, err = qr.data.DB.Context(ctx).Insert(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tquestion.ID = uid.EnShortID(question.ID)\n\t}\n\treturn\n}\n\n// RemoveQuestion delete question\nfunc (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) {\n\tid = uid.DeShortID(id)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", id).Delete(&entity.Question{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateQuestion update question\nfunc (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, cols []string) (err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", question.ID).Cols(cols...).Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tquestion.ID = uid.EnShortID(question.ID)\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn\n}\n\nfunc (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) {\n\tquestionID = uid.DeShortID(questionID)\n\tquestion := &entity.Question{}\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", questionID).Incr(\"view_count\", 1).Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) {\n\tquestionID = uid.DeShortID(questionID)\n\tquestion := &entity.Question{}\n\tquestion.AnswerCount = num\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", questionID).Cols(\"answer_count\").Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) {\n\tquestionID = uid.DeShortID(questionID)\n\t_, err = qr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\tsession = session.Context(ctx)\n\t\tcount, err = session.Count(&entity.Collection{ObjectID: questionID})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tquestion := &entity.Question{CollectionCount: int(count)}\n\t\t_, err = session.ID(questionID).MustCols(\"collection_count\").Update(question)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn\n\t})\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) {\n\tquestionID = uid.DeShortID(questionID)\n\t_, err = qr.data.DB.Context(ctx).ID(questionID).Cols(\"status\").Update(&entity.Question{Status: status})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, questionID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", question.ID).Cols(\"status\").Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) {\n\t// get all deleted question ids\n\tids := make([]string, 0)\n\terr = qr.data.DB.Context(ctx).Select(\"id\").Table(new(entity.Question).TableName()).\n\t\tWhere(\"status = ?\", entity.QuestionStatusDeleted).Find(&ids)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil\n\t}\n\n\t// delete all revisions permanently\n\t_, err = qr.data.DB.Context(ctx).In(\"object_id\", ids).Delete(&entity.Revision{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t_, err = qr.data.DB.Context(ctx).Where(\"status = ?\", entity.QuestionStatusDeleted).Delete(&entity.Question{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) {\n\tquestionID = uid.DeShortID(questionID)\n\t_, err = qr.data.DB.Context(ctx).ID(questionID).Cols(\"status\").Update(&entity.Question{Status: entity.QuestionStatusAvailable})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, questionID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", question.ID).Cols(\"pin\", \"show\").Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", question.ID).Cols(\"accepted_answer_id\").Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn nil\n}\n\nfunc (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\t_, err = qr.data.DB.Context(ctx).Where(\"id =?\", question.ID).Cols(\"last_answer_id\").Update(question)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_ = qr.UpdateSearch(ctx, question.ID)\n\treturn nil\n}\n\n// GetQuestion get question one\nfunc (qr *questionRepo) GetQuestion(ctx context.Context, id string) (\n\tquestion *entity.Question, exist bool, err error,\n) {\n\tid = uid.DeShortID(id)\n\tquestion = &entity.Question{}\n\tquestion.ID = id\n\texist, err = qr.data.DB.Context(ctx).Where(\"id = ?\", id).Get(question)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tquestion.ID = uid.EnShortID(question.ID)\n\t}\n\treturn\n}\n\n// GetQuestionsByTitle get question list by title\nfunc (qr *questionRepo) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (\n\tquestionList []*entity.Question, err error) {\n\tquestionList = make([]*entity.Question, 0)\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Where(\"status != ?\", entity.QuestionStatusDeleted)\n\tsession.Where(\"title like ?\", \"%\"+title+\"%\")\n\tsession.Limit(pageSize)\n\terr = session.Find(&questionList)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range questionList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) {\n\tfor key, itemID := range id {\n\t\tid[key] = uid.DeShortID(itemID)\n\t}\n\tquestionList = make([]*entity.Question, 0)\n\terr = qr.data.DB.Context(ctx).Table(\"question\").In(\"id\", id).Find(&questionList)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range questionList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\treturn\n}\n\n// GetQuestionList get question list all\nfunc (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) {\n\tquestion.ID = uid.DeShortID(question.ID)\n\tquestionList = make([]*entity.Question, 0)\n\terr = qr.data.DB.Context(ctx).Find(&questionList, question)\n\tif err != nil {\n\t\treturn questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tfor _, item := range questionList {\n\t\titem.ID = uid.DeShortID(item.ID)\n\t}\n\treturn\n}\n\nfunc (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Where(builder.Lt{\"status\": entity.QuestionStatusDeleted})\n\tcount, err = session.Count(&entity.Question{Show: entity.QuestionShow})\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (qr *questionRepo) GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) {\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Where(builder.Lt{\"status\": entity.QuestionStatusDeleted}).\n\t\tAnd(builder.Eq{\"answer_count\": 0})\n\tcount, err = session.Count(&entity.Question{Show: entity.QuestionShow})\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (qr *questionRepo) GetResolvedQuestionCount(ctx context.Context) (count int64, err error) {\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Where(builder.Lt{\"status\": entity.QuestionStatusDeleted}).\n\t\tAnd(builder.Neq{\"answer_count\": 0}).\n\t\tAnd(builder.Neq{\"accepted_answer_id\": 0})\n\tcount, err = session.Count(&entity.Question{Show: entity.QuestionShow})\n\tif err != nil {\n\t\treturn 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) {\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Where(builder.Lt{\"status\": entity.QuestionStatusDeleted})\n\tcount, err = session.Count(&entity.Question{UserID: userID, Show: show})\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) (\n\tquestionIDList []*schema.SiteMapQuestionInfo, err error) {\n\tpage--\n\tquestionIDList = make([]*schema.SiteMapQuestionInfo, 0)\n\n\t// try to get sitemap data from cache\n\tcacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page)\n\tcacheData, exist, err := qr.data.Cache.GetString(ctx, cacheKey)\n\tif err == nil && exist {\n\t\t_ = json.Unmarshal([]byte(cacheData), &questionIDList)\n\t\treturn questionIDList, nil\n\t}\n\n\t// get sitemap data from db\n\trows := make([]*entity.Question, 0)\n\tsession := qr.data.DB.Context(ctx)\n\tsession.Select(\"id,title,created_at,post_update_time\")\n\tsession.Where(\"`show` = ?\", entity.QuestionShow)\n\tsession.Where(\"status = ? OR status = ?\", entity.QuestionStatusAvailable, entity.QuestionStatusClosed)\n\tsession.Limit(pageSize, page*pageSize)\n\tsession.Asc(\"created_at\")\n\terr = session.Find(&rows)\n\tif err != nil {\n\t\treturn questionIDList, err\n\t}\n\n\t// warp data\n\tfor _, question := range rows {\n\t\titem := &schema.SiteMapQuestionInfo{ID: question.ID}\n\t\tif handler.GetEnableShortID(ctx) {\n\t\t\titem.ID = uid.EnShortID(question.ID)\n\t\t}\n\t\titem.Title = htmltext.UrlTitle(question.Title)\n\t\tif question.PostUpdateTime.IsZero() {\n\t\t\titem.UpdateTime = question.CreatedAt.Format(time.RFC3339)\n\t\t} else {\n\t\t\titem.UpdateTime = question.PostUpdateTime.Format(time.RFC3339)\n\t\t}\n\t\tquestionIDList = append(questionIDList, item)\n\t}\n\n\t// set sitemap data to cache\n\tcacheDataByte, _ := json.Marshal(questionIDList)\n\tif err := qr.data.Cache.SetString(ctx, cacheKey, string(cacheDataByte), constant.SiteMapQuestionCacheTime); err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn questionIDList, nil\n}\n\n// GetQuestionPage query question page\nfunc (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,\n\ttagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) (\n\tquestionList []*entity.Question, total int64, err error) {\n\tquestionList = make([]*entity.Question, 0)\n\tsession := qr.data.DB.Context(ctx)\n\tstatus := []int{entity.QuestionStatusAvailable}\n\tif orderCond != \"unanswered\" {\n\t\tstatus = append(status, entity.QuestionStatusClosed)\n\t}\n\tif showPending {\n\t\tstatus = append(status, entity.QuestionStatusPending)\n\t}\n\tsession.Select(\"question.*\")\n\tsession.In(\"question.status\", status)\n\tif len(tagIDs) > 0 {\n\t\tsession.Join(\"LEFT\", \"tag_rel\", \"question.id = tag_rel.object_id\")\n\t\tsession.In(\"tag_rel.tag_id\", tagIDs)\n\t\tsession.And(\"tag_rel.status = ?\", entity.TagRelStatusAvailable)\n\t}\n\tif len(userID) > 0 {\n\t\tsession.And(\"question.user_id = ?\", userID)\n\t\tif !showHidden {\n\t\t\tsession.And(\"question.show = ?\", entity.QuestionShow)\n\t\t}\n\t} else {\n\t\tsession.And(\"question.show = ?\", entity.QuestionShow)\n\t}\n\tif inDays > 0 {\n\t\tsession.And(\"question.created_at > ?\", time.Now().AddDate(0, 0, -inDays))\n\t}\n\n\tswitch orderCond {\n\tcase \"newest\":\n\t\tsession.OrderBy(\"question.pin desc,question.created_at DESC\")\n\tcase \"active\":\n\t\tif inDays == 0 {\n\t\t\tsession.And(\"question.created_at > ?\", time.Now().AddDate(0, 0, -180))\n\t\t}\n\t\tsession.And(\"question.post_update_time > ?\", time.Now().AddDate(0, 0, -90))\n\t\tsession.OrderBy(\"question.pin desc,question.post_update_time DESC, question.updated_at DESC\")\n\tcase \"hot\":\n\t\tsession.OrderBy(\"question.pin desc,question.hot_score DESC\")\n\tcase \"score\":\n\t\tsession.OrderBy(\"question.pin desc,question.vote_count DESC, question.view_count DESC\")\n\tcase \"unanswered\":\n\t\tsession.Where(\"question.answer_count = 0\")\n\t\tsession.OrderBy(\"question.pin desc,question.created_at DESC\")\n\tcase \"frequent\":\n\t\tsession.OrderBy(\"question.pin DESC, question.linked_count DESC, question.updated_at DESC\")\n\t}\n\n\tsession.GroupBy(\"question.id\")\n\ttotal, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range questionList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\treturn questionList, total, err\n}\n\n// GetRecommendQuestionPageByTags get recommend question page by tags\nfunc (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (\n\tquestionList []*entity.Question, total int64, err error) {\n\tquestionList = make([]*entity.Question, 0)\n\torderBySQL := \"question.pin DESC, question.created_at DESC\"\n\n\t// Please Make sure every question has at least one tag\n\tselectSQL := entity.Question{}.TableName() + \".*\"\n\tif len(followedQuestionIDs) > 0 {\n\t\tidStr := \"'\" + strings.Join(followedQuestionIDs, \"','\") + \"'\"\n\t\tselectSQL += fmt.Sprintf(\", CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END AS order_priority\", idStr)\n\t\torderBySQL = \"order_priority, \" + orderBySQL\n\t}\n\tsession := qr.data.DB.Context(ctx).Select(selectSQL)\n\n\tif len(tagIDs) > 0 {\n\t\tsession.Where(\"question.user_id != ?\", userID).\n\t\t\tAnd(\"question.id NOT IN (SELECT question_id FROM answer WHERE user_id = ?)\", userID).\n\t\t\tJoin(\"INNER\", \"tag_rel\", \"question.id = tag_rel.object_id\").\n\t\t\tAnd(\"tag_rel.status = ?\", entity.TagRelStatusAvailable).\n\t\t\tJoin(\"INNER\", \"tag\", \"tag.id = tag_rel.tag_id\").\n\t\t\tIn(\"tag.id\", tagIDs)\n\t} else if len(followedQuestionIDs) == 0 {\n\t\treturn questionList, 0, nil\n\t}\n\n\tif len(followedQuestionIDs) > 0 {\n\t\tif len(tagIDs) > 0 {\n\t\t\t// if tags provided, show followed questions and tag questions\n\t\t\tsession.Or(builder.In(\"question.id\", followedQuestionIDs))\n\t\t} else {\n\t\t\t// if no tags, only show followed questions\n\t\t\tsession.Where(builder.In(\"question.id\", followedQuestionIDs))\n\t\t}\n\t}\n\n\tsession.\n\t\tAnd(\"question.show = ? and question.status = ?\", entity.QuestionShow, entity.QuestionStatusAvailable).\n\t\tDistinct(\"question.id\").\n\t\tOrderBy(orderBySQL)\n\n\ttotal, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range questionList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\n\treturn questionList, total, err\n}\n\nfunc (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) {\n\tvar (\n\t\tcount   int64\n\t\terr     error\n\t\tsession = qr.data.DB.Context(ctx).Table(\"question\")\n\t)\n\n\tsession.Where(builder.Eq{\n\t\t\"status\": search.Status,\n\t})\n\n\trows := make([]*entity.Question, 0)\n\tif search.Page > 0 {\n\t\tsearch.Page--\n\t} else {\n\t\tsearch.Page = 0\n\t}\n\tif search.PageSize == 0 {\n\t\tsearch.PageSize = constant.DefaultPageSize\n\t}\n\n\t// search by question title like or question id\n\tif len(search.Query) > 0 {\n\t\t// check id search\n\t\tvar (\n\t\t\tidSearch = false\n\t\t\tid       = \"\"\n\t\t)\n\n\t\tif strings.Contains(search.Query, \"question:\") {\n\t\t\tidSearch = true\n\t\t\tid = strings.TrimSpace(strings.TrimPrefix(search.Query, \"question:\"))\n\t\t\tid = uid.DeShortID(id)\n\t\t\tfor _, r := range id {\n\t\t\t\tif !unicode.IsDigit(r) {\n\t\t\t\t\tidSearch = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif idSearch {\n\t\t\tsession.And(builder.Eq{\n\t\t\t\t\"id\": id,\n\t\t\t})\n\t\t} else {\n\t\t\tsession.And(builder.Like{\n\t\t\t\t\"title\", search.Query,\n\t\t\t})\n\t\t}\n\t}\n\n\toffset := search.Page * search.PageSize\n\n\tsession.OrderBy(\"created_at desc\").\n\t\tLimit(search.PageSize, offset)\n\tcount, err = session.FindAndCount(&rows)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn rows, count, err\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range rows {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\treturn rows, count, nil\n}\n\n// UpdateSearch update search, if search plugin not enable, do nothing\nfunc (qr *questionRepo) UpdateSearch(ctx context.Context, questionID string) (err error) {\n\t// check search plugin\n\tvar s plugin.Search\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\ts = search\n\t\treturn nil\n\t})\n\tif s == nil {\n\t\treturn\n\t}\n\tquestionID = uid.DeShortID(questionID)\n\tquestion, exist, err := qr.GetQuestion(ctx, questionID)\n\tif !exist {\n\t\treturn\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// get tags\n\tvar (\n\t\ttagListList = make([]*entity.TagRel, 0)\n\t\ttags        = make([]string, 0)\n\t)\n\tsession := qr.data.DB.Context(ctx).Where(\"object_id = ?\", questionID)\n\tsession.Where(\"status = ?\", entity.TagRelStatusAvailable)\n\terr = session.Find(&tagListList)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tfor _, tag := range tagListList {\n\t\ttags = append(tags, tag.TagID)\n\t}\n\tcontent := &plugin.SearchContent{\n\t\tObjectID:    questionID,\n\t\tTitle:       question.Title,\n\t\tType:        constant.QuestionObjectType,\n\t\tContent:     question.OriginalText,\n\t\tAnswers:     int64(question.AnswerCount),\n\t\tStatus:      plugin.SearchContentStatus(question.Status),\n\t\tTags:        tags,\n\t\tQuestionID:  questionID,\n\t\tUserID:      question.UserID,\n\t\tViews:       int64(question.ViewCount),\n\t\tCreated:     question.CreatedAt.Unix(),\n\t\tActive:      question.UpdatedAt.Unix(),\n\t\tScore:       int64(question.VoteCount),\n\t\tHasAccepted: question.AcceptedAnswerID != \"\" && question.AcceptedAnswerID != \"0\",\n\t}\n\terr = s.UpdateContent(ctx, content)\n\treturn\n}\n\nfunc (qr *questionRepo) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) {\n\t// get all question id that need to be deleted\n\tquestionIDs := make([]string, 0)\n\tsession := qr.data.DB.Context(ctx).Where(\"user_id = ?\", userID)\n\tsession.Where(\"status != ?\", entity.QuestionStatusDeleted)\n\terr = session.Select(\"id\").Table(\"question\").Find(&questionIDs)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif len(questionIDs) == 0 {\n\t\treturn nil\n\t}\n\n\tlog.Infof(\"find %d questions need to be deleted for user %s\", len(questionIDs), userID)\n\n\t// delete all question\n\tsession = qr.data.DB.Context(ctx).Where(\"user_id = ?\", userID)\n\tsession.Where(\"status != ?\", entity.QuestionStatusDeleted)\n\t_, err = session.Cols(\"status\", \"updated_at\").Update(&entity.Question{\n\t\tUpdatedAt: time.Now(),\n\t\tStatus:    entity.QuestionStatusDeleted,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// update search content\n\tfor _, id := range questionIDs {\n\t\t_ = qr.UpdateSearch(ctx, id)\n\t}\n\treturn nil\n}\n\n// LinkQuestion batch insert question link\nfunc (qr *questionRepo) LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) {\n\t// Batch retrieve all links\n\tvar links []*entity.QuestionLink\n\tfor _, l := range link {\n\t\tl.FromQuestionID = uid.DeShortID(l.FromQuestionID)\n\t\tl.ToQuestionID = uid.DeShortID(l.ToQuestionID)\n\t\tl.FromAnswerID = uid.DeShortID(l.FromAnswerID)\n\t\tl.ToAnswerID = uid.DeShortID(l.ToAnswerID)\n\t\tlinks = append(links, l)\n\t}\n\t// Retrieve existing records from the database\n\tvar existLinks []*entity.QuestionLink\n\tsession := qr.data.DB.Context(ctx)\n\tfor _, link := range links {\n\t\tsession = session.Or(builder.Eq{\n\t\t\t\"from_question_id\": link.FromQuestionID,\n\t\t\t\"to_question_id\":   link.ToQuestionID,\n\t\t\t\"from_answer_id\":   link.FromAnswerID,\n\t\t\t\"to_answer_id\":     link.ToAnswerID,\n\t\t})\n\t}\n\terr = session.Find(&existLinks)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// Optimize separation of records that need to be updated or inserted using a map\n\texistMap := make(map[string]*entity.QuestionLink)\n\tfor _, el := range existLinks {\n\t\tkey := fmt.Sprintf(\"%s:%s:%s:%s\", el.FromQuestionID, el.ToQuestionID, el.FromAnswerID, el.ToAnswerID)\n\t\texistMap[key] = el\n\t}\n\n\tvar updateLinks []*entity.QuestionLink\n\tvar insertLinks []*entity.QuestionLink\n\tfor _, link := range links {\n\t\tkey := fmt.Sprintf(\"%s:%s:%s:%s\", link.FromQuestionID, link.ToQuestionID, link.FromAnswerID, link.ToAnswerID)\n\t\tif el, exist := existMap[key]; exist {\n\t\t\tif el.Status == entity.QuestionLinkStatusDeleted {\n\t\t\t\tel.Status = entity.QuestionLinkStatusAvailable\n\t\t\t\tel.UpdatedAt = time.Now()\n\t\t\t\tupdateLinks = append(updateLinks, el)\n\t\t\t}\n\t\t} else {\n\t\t\tlink.Status = entity.QuestionLinkStatusAvailable\n\t\t\tlink.CreatedAt = time.Now()\n\t\t\tlink.UpdatedAt = time.Now()\n\t\t\tinsertLinks = append(insertLinks, link)\n\t\t}\n\t}\n\n\t// Batch update\n\tif len(updateLinks) > 0 {\n\t\tfor _, link := range updateLinks {\n\t\t\t_, err = qr.data.DB.Context(ctx).ID(link.ID).Cols(\"status\").Update(&entity.QuestionLink{Status: entity.QuestionLinkStatusAvailable})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t\t}\n\t\t}\n\t}\n\n\t// Batch insert\n\tif len(insertLinks) > 0 {\n\t\t_, err = qr.data.DB.Context(ctx).Insert(insertLinks)\n\t\tif err != nil {\n\t\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t}\n\n\treturn\n}\n\n// UpdateQuestionLinkCount update question link count\nfunc (qr *questionRepo) UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) {\n\t// count the number of links\n\tcount, err := qr.data.DB.Context(ctx).\n\t\tWhere(\"to_question_id = ?\", questionID).\n\t\tWhere(\"status = ?\", entity.QuestionLinkStatusAvailable).\n\t\tCount(&entity.QuestionLink{})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\t// update the number of links\n\t_, err = qr.data.DB.Context(ctx).ID(questionID).\n\t\tCols(\"linked_count\").Update(&entity.Question{LinkedCount: int(count)})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetLinkedQuestionIDs get linked question ids\nfunc (qr *questionRepo) GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (\n\tquestionIDs []string, err error) {\n\tquestionIDs = make([]string, 0)\n\terr = qr.data.DB.Context(ctx).\n\t\tSelect(\"to_question_id\").\n\t\tTable(new(entity.QuestionLink).TableName()).\n\t\tWhere(\"from_question_id = ?\", questionID).\n\t\tWhere(\"status = ?\", status).\n\t\tFind(&questionIDs)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn questionIDs, nil\n}\n\n// RecoverQuestionLink batch recover question link\nfunc (qr *questionRepo) RecoverQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) {\n\treturn qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusAvailable, links...)\n}\n\n// RemoveQuestionLink batch remove question link\nfunc (qr *questionRepo) RemoveQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) {\n\treturn qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusDeleted, links...)\n}\n\n// UpdateQuestionLinkStatus update question link status\nfunc (qr *questionRepo) UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) {\n\tif len(links) == 0 {\n\t\treturn nil\n\t}\n\n\tsession := qr.data.DB.Context(ctx).Cols(\"status\")\n\tfor _, link := range links {\n\t\teq := builder.Eq{}\n\t\tif link.FromQuestionID != \"\" {\n\t\t\teq[\"from_question_id\"] = uid.DeShortID(link.FromQuestionID)\n\t\t}\n\t\tif link.FromAnswerID != \"\" {\n\t\t\teq[\"from_answer_id\"] = uid.DeShortID(link.FromAnswerID)\n\t\t}\n\t\tif link.ToQuestionID != \"\" {\n\t\t\teq[\"to_question_id\"] = uid.DeShortID(link.ToQuestionID)\n\t\t}\n\t\tif link.ToAnswerID != \"\" {\n\t\t\teq[\"to_answer_id\"] = uid.DeShortID(link.ToAnswerID)\n\t\t}\n\t\tsession = session.Or(eq)\n\t}\n\t_, err = session.Update(&entity.QuestionLink{Status: status})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetQuestionLink get linked question to questionID\nfunc (qr *questionRepo) GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questionList []*entity.Question, total int64, err error) {\n\tquestionList = make([]*entity.Question, 0)\n\tquestionID = uid.DeShortID(questionID)\n\tquestionStatus := []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed, entity.QuestionStatusPending}\n\tif questionID == \"0\" {\n\t\treturn nil, 0, errors.InternalServer(reason.DatabaseError).WithError(\n\t\t\tfmt.Errorf(\"questionID is empty\"),\n\t\t).WithStack()\n\t}\n\n\tsession := qr.data.DB.Context(ctx).\n\t\tTable(\"question_link\").\n\t\tJoin(\"INNER\", \"question\", \"question_link.from_question_id = question.id\").\n\t\tWhere(\"question_link.to_question_id = ? AND question.show = ?\", questionID, entity.QuestionShow).\n\t\tDistinct(\"question.id\").\n\t\tWhere(\"question_link.status = ?\", entity.QuestionLinkStatusAvailable).\n\t\tSelect(\"question.*\").\n\t\tIn(\"question.status\", questionStatus)\n\n\tswitch orderCond {\n\tcase \"newest\":\n\t\tsession.OrderBy(\"question.pin desc,question.created_at DESC\")\n\tcase \"active\":\n\t\tif inDays == 0 {\n\t\t\tsession.And(\"question.created_at > ?\", time.Now().AddDate(0, 0, -180))\n\t\t}\n\t\tsession.And(\"question.post_update_time > ?\", time.Now().AddDate(0, 0, -90))\n\t\tsession.OrderBy(\"question.pin desc,question.post_update_time DESC, question.updated_at DESC\")\n\tcase \"hot\":\n\t\tsession.OrderBy(\"question.pin desc,question.hot_score DESC\")\n\tcase \"score\":\n\t\tsession.OrderBy(\"question.pin desc,question.vote_count DESC, question.view_count DESC\")\n\tcase \"unanswered\":\n\t\tsession.Where(\"question.answer_count = 0\")\n\t\tsession.OrderBy(\"question.pin desc,question.created_at DESC\")\n\tcase \"frequent\":\n\t\tsession.OrderBy(\"question.pin DESC, question.linked_count DESC, question.updated_at DESC\")\n\t}\n\n\tif page > 0 && pageSize > 0 {\n\t\tsession.Limit(pageSize, (page-1)*pageSize)\n\t}\n\n\ttotal, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range questionList {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/rank/user_rank_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage rank\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/jinzhu/now\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// UserRankRepo user rank repository\ntype UserRankRepo struct {\n\tdata          *data.Data\n\tconfigService *config.ConfigService\n}\n\n// NewUserRankRepo new repository\nfunc NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.UserRankRepo {\n\treturn &UserRankRepo{\n\t\tdata:          data,\n\t\tconfigService: configService,\n\t}\n}\n\nfunc (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) {\n\tmaxDailyRank, err = ur.configService.GetIntValue(ctx, \"daily_rank_limit\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn maxDailyRank, nil\n}\n\nfunc (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session,\n\tuserID string, maxDailyRank int) (\n\treach bool, err error) {\n\tsession.Where(builder.Eq{\"user_id\": userID})\n\tsession.Where(builder.Eq{\"cancelled\": 0})\n\tsession.Where(builder.Between{\n\t\tCol:     \"updated_at\",\n\t\tLessVal: now.BeginningOfDay(),\n\t\tMoreVal: now.EndOfDay(),\n\t})\n\n\tearned, err := session.SumInt(&entity.Activity{}, \"`rank`\")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif int(earned) < maxDailyRank {\n\t\treturn false, nil\n\t}\n\tlog.Infof(\"user %s today has rank %d is reach stand %d\", userID, earned, maxDailyRank)\n\treturn true, nil\n}\n\n// ChangeUserRank change user rank\nfunc (ur *UserRankRepo) ChangeUserRank(\n\tctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) {\n\t// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.\n\tif plugin.RankAgentEnabled() || deltaRank == 0 {\n\t\treturn nil\n\t}\n\n\t// If user rank is lower than 1 after this action, then user rank will be set to 1 only.\n\tif deltaRank < 0 && userCurrentScore+deltaRank < 1 {\n\t\tdeltaRank = 1 - userCurrentScore\n\t}\n\n\t_, err = session.ID(userID).Incr(\"`rank`\", deltaRank).Update(&entity.User{})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// TriggerUserRank trigger user rank change\n// session is need provider, it means this action must be success or failure\n// if outer action is failed then this action is need rollback\nfunc (ur *UserRankRepo) TriggerUserRank(ctx context.Context,\n\tsession *xorm.Session, userID string, deltaRank int, activityType int,\n) (isReachStandard bool, err error) {\n\t// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.\n\tif plugin.RankAgentEnabled() || deltaRank == 0 {\n\t\treturn false, nil\n\t}\n\n\tif deltaRank < 0 {\n\t\t// if user rank is lower than 1 after this action, then user rank will be set to 1 only.\n\t\tvar isReachMin bool\n\t\tisReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank)\n\t\tif err != nil {\n\t\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif isReachMin {\n\t\t\t_, err = session.Where(builder.Eq{\"id\": userID}).Update(&entity.User{Rank: 1})\n\t\t\tif err != nil {\n\t\t\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t\t}\n\t\t\treturn true, nil\n\t\t}\n\t} else {\n\t\tisReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType)\n\t\tif err != nil {\n\t\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif isReachStandard {\n\t\t\treturn isReachStandard, nil\n\t\t}\n\t}\n\t_, err = session.Where(builder.Eq{\"id\": userID}).Incr(\"`rank`\", deltaRank).Update(&entity.User{})\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn false, nil\n}\n\nfunc (ur *UserRankRepo) checkUserMinRank(_ context.Context, session *xorm.Session, userID string, deltaRank int) (\n\tisReachStandard bool, err error,\n) {\n\tbean := &entity.User{ID: userID}\n\t_, err = session.Select(\"`rank`\").Get(bean)\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif bean.Rank+deltaRank < 1 {\n\t\tlog.Infof(\"user %s is rank %d out of range before rank operation\", userID, deltaRank)\n\t\treturn true, nil\n\t}\n\treturn\n}\n\nfunc (ur *UserRankRepo) checkUserTodayRank(ctx context.Context,\n\tsession *xorm.Session, userID string, activityType int,\n) (isReachStandard bool, err error) {\n\t// exclude daily rank\n\texclude, _ := ur.configService.GetArrayStringValue(ctx, \"daily_rank_limit.exclude\")\n\tfor _, item := range exclude {\n\t\tcfg, err := ur.configService.GetConfigByKey(ctx, item)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif activityType == cfg.ID {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\t// get user\n\tstart, end := now.BeginningOfDay(), now.EndOfDay()\n\tsession.Where(builder.Eq{\"user_id\": userID})\n\tsession.Where(builder.Eq{\"cancelled\": 0})\n\tsession.Where(builder.Between{\n\t\tCol:     \"updated_at\",\n\t\tLessVal: start,\n\t\tMoreVal: end,\n\t})\n\tearned, err := session.SumInt(&entity.Activity{}, \"`rank`\")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// max rank\n\tmaxDailyRank, err := ur.configService.GetIntValue(ctx, \"daily_rank_limit\")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif int(earned) < maxDailyRank {\n\t\treturn false, nil\n\t}\n\tlog.Infof(\"user %s today has rank %d is reach stand %d\", userID, earned, maxDailyRank)\n\treturn true, nil\n}\n\nfunc (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, pageSize int) (\n\trankPage []*entity.Activity, total int64, err error,\n) {\n\trankPage = make([]*entity.Activity, 0)\n\n\tsession := ur.data.DB.Context(ctx).Where(builder.Eq{\"has_rank\": 1}.And(builder.Eq{\"cancelled\": 0})).And(builder.Gt{\"`rank`\": 0})\n\tsession.Desc(\"created_at\")\n\n\tcond := &entity.Activity{UserID: userID}\n\ttotal, err = pager.Help(page, pageSize, &rankPage, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/reason/reason_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage reason\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/reason_common\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype reasonRepo struct {\n\tconfigService *config.ConfigService\n}\n\nfunc NewReasonRepo(configService *config.ConfigService) reason_common.ReasonRepo {\n\treturn &reasonRepo{\n\t\tconfigService: configService,\n\t}\n}\n\nfunc (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) {\n\tlang := handler.GetLangByCtx(ctx)\n\treasonAction := fmt.Sprintf(\"%s.%s.reasons\", objectType, action)\n\tresp = make([]*schema.ReasonItem, 0)\n\n\treasonKeys, err := rr.configService.GetArrayStringValue(ctx, reasonAction)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, reasonKey := range reasonKeys {\n\t\tcfg, err := rr.configService.GetConfigByKey(ctx, reasonKey)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\treason := &schema.ReasonItem{}\n\t\terr = json.Unmarshal(cfg.GetByteValue(), reason)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\treason.Translate(reasonKey, lang)\n\t\treason.ReasonType = cfg.ID\n\t\tresp = append(resp, reason)\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/repo/repo_test/auth_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/auth\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\taccessToken = \"token\"\n\tvisitToken  = \"visitToken\"\n\tuserID      = \"1\"\n)\n\nfunc Test_authRepo_SetUserCacheInfo(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\tcacheInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\tassert.Equal(t, userID, cacheInfo.UserID)\n}\n\nfunc Test_authRepo_RemoveUserCacheInfo(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\terr = authRepo.RemoveUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\n\tuserInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\tassert.Nil(t, userInfo)\n}\n\nfunc Test_authRepo_SetUserStatus(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\tcacheInfo, err := authRepo.GetUserStatus(context.TODO(), userID)\n\trequire.NoError(t, err)\n\tassert.Equal(t, userID, cacheInfo.UserID)\n}\nfunc Test_authRepo_RemoveUserStatus(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\terr = authRepo.RemoveUserStatus(context.TODO(), userID)\n\trequire.NoError(t, err)\n\n\tuserInfo, err := authRepo.GetUserStatus(context.TODO(), userID)\n\trequire.NoError(t, err)\n\tassert.Nil(t, userInfo)\n}\n\nfunc Test_authRepo_SetAdminUserCacheInfo(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\tcacheInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\tassert.Equal(t, userID, cacheInfo.UserID)\n}\n\nfunc Test_authRepo_RemoveAdminUserCacheInfo(t *testing.T) {\n\tauthRepo := auth.NewAuthRepo(testDataSource)\n\n\terr := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID})\n\trequire.NoError(t, err)\n\n\terr = authRepo.RemoveAdminUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\n\tuserInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken)\n\trequire.NoError(t, err)\n\tassert.Nil(t, userInfo)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/captcha_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/repo/captcha\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tip         = \"127.0.0.1\"\n\tactionType = \"actionType\"\n\tamount     = 1\n)\n\nfunc Test_captchaRepo_DelActionType(t *testing.T) {\n\tcaptchaRepo := captcha.NewCaptchaRepo(testDataSource)\n\terr := captchaRepo.SetActionType(context.TODO(), ip, actionType, \"\", amount)\n\trequire.NoError(t, err)\n\n\tactionInfo, err := captchaRepo.GetActionType(context.TODO(), ip, actionType)\n\trequire.NoError(t, err)\n\tassert.Equal(t, amount, actionInfo.Num)\n\n\terr = captchaRepo.DelActionType(context.TODO(), ip, actionType)\n\trequire.NoError(t, err)\n}\n\nfunc Test_captchaRepo_SetCaptcha(t *testing.T) {\n\tcaptchaRepo := captcha.NewCaptchaRepo(testDataSource)\n\tkey, capt := \"key\", \"1234\"\n\terr := captchaRepo.SetCaptcha(context.TODO(), key, capt)\n\trequire.NoError(t, err)\n\n\tgotCaptcha, err := captchaRepo.GetCaptcha(context.TODO(), key)\n\trequire.NoError(t, err)\n\tassert.Equal(t, capt, gotCaptcha)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/comment_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/comment\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\tcommentService \"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc buildCommentEntity() *entity.Comment {\n\treturn &entity.Comment{\n\t\tUserID:       \"1\",\n\t\tObjectID:     \"1\",\n\t\tQuestionID:   \"1\",\n\t\tVoteCount:    1,\n\t\tStatus:       entity.CommentStatusAvailable,\n\t\tOriginalText: \"# title\",\n\t\tParsedText:   \"<h1>Title</h1>\",\n\t}\n}\n\nfunc Test_commentRepo_AddComment(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\tcommentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)\n\ttestCommentEntity := buildCommentEntity()\n\terr := commentRepo.AddComment(context.TODO(), testCommentEntity)\n\trequire.NoError(t, err)\n\n\terr = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_commentRepo_GetCommentPage(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\tcommentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)\n\ttestCommentEntity := buildCommentEntity()\n\terr := commentRepo.AddComment(context.TODO(), testCommentEntity)\n\trequire.NoError(t, err)\n\n\tresp, total, err := commentRepo.GetCommentPage(context.TODO(), &commentService.CommentQuery{\n\t\tPageCond: pager.PageCond{\n\t\t\tPage:     1,\n\t\t\tPageSize: 10,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), total)\n\tassert.Equal(t, resp[0].ID, testCommentEntity.ID)\n\n\terr = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_commentRepo_UpdateComment(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\tcommentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)\n\tcommonCommentRepo := comment.NewCommentCommonRepo(testDataSource, uniqueIDRepo)\n\ttestCommentEntity := buildCommentEntity()\n\terr := commentRepo.AddComment(context.TODO(), testCommentEntity)\n\trequire.NoError(t, err)\n\n\ttestCommentEntity.ParsedText = \"test\"\n\terr = commentRepo.UpdateCommentContent(context.TODO(), testCommentEntity.ID, \"test\", \"test\")\n\trequire.NoError(t, err)\n\n\tnewComment, exist, err := commonCommentRepo.GetComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, testCommentEntity.ParsedText, newComment.ParsedText)\n\n\terr = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_commentRepo_CannotGetDeletedComment(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\tcommentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)\n\ttestCommentEntity := buildCommentEntity()\n\n\terr := commentRepo.AddComment(context.TODO(), testCommentEntity)\n\trequire.NoError(t, err)\n\n\terr = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n\n\t_, exist, err := commentRepo.GetComment(context.TODO(), testCommentEntity.ID)\n\trequire.NoError(t, err)\n\tassert.False(t, exist)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/email_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/repo/export\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_emailRepo_VerifyCode(t *testing.T) {\n\temailRepo := export.NewEmailRepo(testDataSource)\n\tcode, content := \"1111\", \"{\\\"source_type\\\":\\\"\\\",\\\"e_mail\\\":\\\"\\\",\\\"user_id\\\":\\\"1\\\",\\\"skip_validation_latest_code\\\":false}\"\n\terr := emailRepo.SetCode(context.TODO(), \"1\", code, content, time.Minute)\n\trequire.NoError(t, err)\n\n\tverifyContent, err := emailRepo.VerifyCode(context.TODO(), code)\n\trequire.NoError(t, err)\n\tassert.Equal(t, content, verifyContent)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/meta_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/meta\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc buildMetaEntity() *entity.Meta {\n\treturn &entity.Meta{\n\t\tObjectID: \"1\",\n\t\tKey:      \"1\",\n\t\tValue:    \"1\",\n\t}\n}\n\nfunc Test_metaRepo_GetMetaByObjectIdAndKey(t *testing.T) {\n\tmetaRepo := meta.NewMetaRepo(testDataSource)\n\tmetaEnt := buildMetaEntity()\n\n\terr := metaRepo.AddMeta(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\n\tgotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, metaEnt.ID, gotMeta.ID)\n\n\terr = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_metaRepo_GetMetaList(t *testing.T) {\n\tmetaRepo := meta.NewMetaRepo(testDataSource)\n\tmetaEnt := buildMetaEntity()\n\n\terr := metaRepo.AddMeta(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\n\tgotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\tassert.Len(t, gotMetaList, 1)\n\tassert.Equal(t, gotMetaList[0].ID, metaEnt.ID)\n\n\terr = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_metaRepo_GetMetaPage(t *testing.T) {\n\tmetaRepo := meta.NewMetaRepo(testDataSource)\n\tmetaEnt := buildMetaEntity()\n\n\terr := metaRepo.AddMeta(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\n\tgotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\tassert.Len(t, gotMetaList, 1)\n\tassert.Equal(t, gotMetaList[0].ID, metaEnt.ID)\n\n\terr = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)\n\trequire.NoError(t, err)\n}\n\nfunc Test_metaRepo_UpdateMeta(t *testing.T) {\n\tmetaRepo := meta.NewMetaRepo(testDataSource)\n\tmetaEnt := buildMetaEntity()\n\n\terr := metaRepo.AddMeta(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\n\tmetaEnt.Value = \"testing\"\n\terr = metaRepo.UpdateMeta(context.TODO(), metaEnt)\n\trequire.NoError(t, err)\n\n\tgotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, gotMeta.Value, metaEnt.Value)\n\n\terr = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/notification_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/notification\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc buildNotificationEntity() *entity.Notification {\n\treturn &entity.Notification{\n\t\tUserID:   \"1\",\n\t\tObjectID: \"1\",\n\t\tContent:  \"1\",\n\t\tType:     schema.NotificationTypeInbox,\n\t\tIsRead:   schema.NotificationNotRead,\n\t\tStatus:   schema.NotificationStatusNormal,\n\t}\n}\n\nfunc Test_notificationRepo_ClearIDUnRead(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\terr = notificationRepo.ClearIDUnRead(context.TODO(), ent.UserID, ent.ID)\n\trequire.NoError(t, err)\n\n\tgot, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.Equal(t, schema.NotificationRead, got.IsRead)\n}\n\nfunc Test_notificationRepo_ClearUnRead(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\terr = notificationRepo.ClearUnRead(context.TODO(), ent.UserID, ent.Type)\n\trequire.NoError(t, err)\n\n\tgot, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.Equal(t, schema.NotificationRead, got.IsRead)\n}\n\nfunc Test_notificationRepo_GetById(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\tgot, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.Equal(t, got.ID, ent.ID)\n}\n\nfunc Test_notificationRepo_GetByUserIdObjectIdTypeId(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\tgot, exists, err := notificationRepo.GetByUserIdObjectIdTypeId(context.TODO(), ent.UserID, ent.ObjectID, ent.Type)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.Equal(t, got.ObjectID, ent.ObjectID)\n}\n\nfunc Test_notificationRepo_GetNotificationPage(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\tnotificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID})\n\trequire.NoError(t, err)\n\tassert.Positive(t, total)\n\tassert.Equal(t, notificationPage[0].UserID, ent.UserID)\n}\n\nfunc Test_notificationRepo_UpdateNotificationContent(t *testing.T) {\n\tnotificationRepo := notification.NewNotificationRepo(testDataSource)\n\tent := buildNotificationEntity()\n\terr := notificationRepo.AddNotification(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\tent.Content = \"test\"\n\terr = notificationRepo.UpdateNotificationContent(context.TODO(), ent)\n\trequire.NoError(t, err)\n\n\tgot, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.Equal(t, got.Content, ent.Content)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/reason_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/repo/config\"\n\tserviceconfig \"github.com/apache/answer/internal/service/config\"\n\n\t\"github.com/apache/answer/internal/repo/reason\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_reasonRepo_ListReasons(t *testing.T) {\n\tconfigRepo := config.NewConfigRepo(testDataSource)\n\treasonRepo := reason.NewReasonRepo(serviceconfig.NewConfigService(configRepo))\n\treasonItems, err := reasonRepo.ListReasons(context.TODO(), \"question\", \"close\")\n\trequire.NoError(t, err)\n\tassert.Len(t, reasonItems, 4)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/recommend_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/activity\"\n\t\"github.com/apache/answer/internal/repo/activity_common\"\n\t\"github.com/apache/answer/internal/repo/config\"\n\t\"github.com/apache/answer/internal/repo/question\"\n\t\"github.com/apache/answer/internal/repo/tag\"\n\t\"github.com/apache/answer/internal/repo/tag_common\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\tconfig2 \"github.com/apache/answer/internal/service/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_questionRepo_GetRecommend(t *testing.T) {\n\tvar (\n\t\tuniqueIDRepo       = unique.NewUniqueIDRepo(testDataSource)\n\t\tquestionRepo       = question.NewQuestionRepo(testDataSource, uniqueIDRepo)\n\t\tuserRepo           = user.NewUserRepo(testDataSource)\n\t\ttagRelRepo         = tag.NewTagRelRepo(testDataSource, uniqueIDRepo)\n\t\ttagRepo            = tag.NewTagRepo(testDataSource, uniqueIDRepo)\n\t\ttagCommenRepo      = tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)\n\t\tconfigRepo         = config.NewConfigRepo(testDataSource)\n\t\tconfigService      = config2.NewConfigService(configRepo)\n\t\tactivityCommonRepo = activity_common.NewActivityRepo(testDataSource, uniqueIDRepo, configService)\n\t\tfollowRepo         = activity.NewFollowRepo(testDataSource, uniqueIDRepo, activityCommonRepo)\n\t)\n\n\t// create question and user\n\tuser := &entity.User{\n\t\tUsername:    \"ferrischi201\",\n\t\tPass:        \"ferrischi201\",\n\t\tEMail:       \"ferrischi201@example.com\",\n\t\tMailStatus:  entity.EmailStatusAvailable,\n\t\tStatus:      entity.UserStatusAvailable,\n\t\tDisplayName: \"ferrischi201\",\n\t\tIsAdmin:     false,\n\t}\n\terr := userRepo.AddUser(context.TODO(), user)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, user.ID)\n\n\tquestions := make([]*entity.Question, 0)\n\t// tag, unjoin, unfollow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       \"1\",\n\t\tTitle:        \"Valid recommendation 1\",\n\t\tOriginalText: \"A go question\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// tag, unjoin, follow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       \"1\",\n\t\tTitle:        \"Valid recommendation 2\",\n\t\tOriginalText: \"A go question\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// tag, join, unfollow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       user.ID,\n\t\tTitle:        \"Invalid recommendation 1\",\n\t\tOriginalText: \"A go question 1\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// tag, join, follow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       user.ID,\n\t\tTitle:        \"Valid recommendation 3\",\n\t\tOriginalText: \"A java question\",\n\t\tParsedText:   \"Java question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// untag, unjoin, unfollow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       \"1\",\n\t\tTitle:        \"Invalid recommendation 2\",\n\t\tOriginalText: \"A go question\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// untag, unjoin, follow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       \"1\",\n\t\tTitle:        \"Valid recommendation 4\",\n\t\tOriginalText: \"A go question\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// untag, join, unfollow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       user.ID,\n\t\tTitle:        \"Invalid recommendation 3\",\n\t\tOriginalText: \"A go question 1\",\n\t\tParsedText:   \"Go question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\t// untag, join, follow\n\tquestions = append(questions, &entity.Question{\n\t\tUserID:       user.ID,\n\t\tTitle:        \"Valid recommendation 5\",\n\t\tOriginalText: \"A java question\",\n\t\tParsedText:   \"Java question\",\n\t\tStatus:       entity.QuestionStatusAvailable,\n\t\tShow:         entity.QuestionShow,\n\t})\n\n\tfor _, question := range questions {\n\t\terr = questionRepo.AddQuestion(context.TODO(), question)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, question.ID)\n\t}\n\n\ttags := []*entity.Tag{\n\t\t{\n\t\t\tSlugName:     \"go\",\n\t\t\tDisplayName:  \"Golang\",\n\t\t\tOriginalText: \"golang\",\n\t\t\tParsedText:   \"<p>golang</p>\",\n\t\t\tStatus:       entity.TagStatusAvailable,\n\t\t},\n\t\t{\n\t\t\tSlugName:     \"java\",\n\t\t\tDisplayName:  \"Java\",\n\t\t\tOriginalText: \"java\",\n\t\t\tParsedText:   \"<p>java</p>\",\n\t\t\tStatus:       entity.TagStatusAvailable,\n\t\t},\n\t}\n\terr = tagCommenRepo.AddTagList(context.TODO(), tags)\n\trequire.NoError(t, err)\n\n\ttagRels := make([]*entity.TagRel, 0)\n\tfor i, question := range questions {\n\t\ttagRel := &entity.TagRel{\n\t\t\tTagID:    tags[i/4].ID,\n\t\t\tObjectID: question.ID,\n\t\t\tStatus:   entity.TagRelStatusAvailable,\n\t\t}\n\t\ttagRels = append(tagRels, tagRel)\n\t}\n\terr = tagRelRepo.AddTagRelList(context.TODO(), tagRels)\n\trequire.NoError(t, err)\n\n\tfollowQuestionIDs := make([]string, 0)\n\tfor i := range questions {\n\t\tif i%2 == 0 {\n\t\t\tcontinue\n\t\t}\n\t\terr = followRepo.Follow(context.TODO(), questions[i].ID, user.ID)\n\t\trequire.NoError(t, err)\n\t\tfollowQuestionIDs = append(followQuestionIDs, questions[i].ID)\n\t}\n\n\t// get recommend\n\tquestionList, total, err := questionRepo.GetRecommendQuestionPageByTags(context.TODO(), user.ID, []string{tags[0].ID}, followQuestionIDs, 1, 20)\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(5), total)\n\tassert.Len(t, questionList, 5)\n\n\t// recovery\n\tt.Cleanup(func() {\n\t\ttagRelIDs := make([]int64, 0)\n\t\tfor i, tagRel := range tagRels {\n\t\t\tif i%2 == 1 {\n\t\t\t\terr = followRepo.FollowCancel(context.TODO(), questions[i].ID, user.ID)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\ttagRelIDs = append(tagRelIDs, tagRel.ID)\n\t\t}\n\t\terr = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs)\n\t\trequire.NoError(t, err)\n\t\tfor _, tag := range tags {\n\t\t\terr = tagRepo.RemoveTag(context.TODO(), tag.ID)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t\tfor _, q := range questions {\n\t\t\terr = questionRepo.RemoveQuestion(context.TODO(), q.ID)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/repo/repo_test/repo_main_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/migrations\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n\t\"github.com/segmentfault/pacman/cache\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n\t\"xorm.io/xorm/schemas\"\n)\n\nvar (\n\tmysqlDBSetting = TestDBSetting{\n\t\tDriver:       string(schemas.MYSQL),\n\t\tImageName:    \"mariadb\",\n\t\tImageVersion: \"10.4.7\",\n\t\tENV:          []string{\"MYSQL_ROOT_PASSWORD=root\", \"MYSQL_DATABASE=answer\", \"MYSQL_ROOT_HOST=%\"},\n\t\tPortID:       \"3306/tcp\",\n\t\tConnection:   \"root:root@(localhost:%s)/answer?parseTime=true\", // port is not fixed, it will be got by port id\n\t}\n\tpostgresDBSetting = TestDBSetting{\n\t\tDriver:       string(schemas.POSTGRES),\n\t\tImageName:    \"postgres\",\n\t\tImageVersion: \"14\",\n\t\tENV:          []string{\"POSTGRES_USER=root\", \"POSTGRES_PASSWORD=root\", \"POSTGRES_DB=answer\", \"LISTEN_ADDRESSES='*'\"},\n\t\tPortID:       \"5432/tcp\",\n\t\tConnection:   \"host=localhost port=%s user=root password=root dbname=answer sslmode=disable\",\n\t}\n\tsqlite3DBSetting = TestDBSetting{\n\t\tDriver:     string(schemas.SQLITE),\n\t\tConnection: filepath.Join(os.TempDir(), \"answer-test-data.db\"),\n\t}\n\tdbSettingMapping = map[string]TestDBSetting{\n\t\tmysqlDBSetting.Driver:    mysqlDBSetting,\n\t\tsqlite3DBSetting.Driver:  sqlite3DBSetting,\n\t\tpostgresDBSetting.Driver: postgresDBSetting,\n\t}\n\t// after all test down will execute tearDown function to clean-up\n\ttearDown func()\n\t// testDataSource used for repo testing\n\ttestDataSource *data.Data\n)\n\nfunc TestMain(t *testing.M) {\n\tdbSetting, ok := dbSettingMapping[os.Getenv(\"TEST_DB_DRIVER\")]\n\tif !ok {\n\t\t// Use sqlite3 to test.\n\t\tdbSetting = dbSettingMapping[string(schemas.SQLITE)]\n\t}\n\tif dbSetting.Driver == string(schemas.SQLITE) {\n\t\t_ = os.RemoveAll(dbSetting.Connection)\n\t}\n\n\tdefer func() {\n\t\tif tearDown != nil {\n\t\t\ttearDown()\n\t\t}\n\t}()\n\tif err := initTestDataSource(dbSetting); err != nil {\n\t\tpanic(err)\n\t}\n\tlog.Info(\"init test database successfully\")\n\n\tif ret := t.Run(); ret != 0 {\n\t\tpanic(ret)\n\t}\n}\n\ntype TestDBSetting struct {\n\tDriver       string\n\tImageName    string\n\tImageVersion string\n\tENV          []string\n\tPortID       string\n\tConnection   string\n}\n\nfunc initTestDataSource(dbSetting TestDBSetting) error {\n\tconnection, imageCleanUp, err := initDatabaseImage(dbSetting)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdbSetting.Connection = connection\n\n\tdbEngine, err := initDatabase(dbSetting)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewCache, err := initCache()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewData, dbCleanUp, err := data.NewData(dbEngine, newCache)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttestDataSource = newData\n\n\ttearDown = func() {\n\t\tdbCleanUp()\n\t\tlog.Info(\"cleanup test database successfully\")\n\t\timageCleanUp()\n\t\tlog.Info(\"cleanup test database image successfully\")\n\t}\n\treturn nil\n}\n\nfunc initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) {\n\t// sqlite3 don't need to set up image\n\tif dbSetting.Driver == string(schemas.SQLITE) {\n\t\treturn dbSetting.Connection, func() {\n\t\t\tlog.Info(\"remove database\", dbSetting.Connection)\n\t\t\terr = os.Remove(dbSetting.Connection)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}, nil\n\t}\n\tpool, err := dockertest.NewPool(\"\")\n\tpool.MaxWait = time.Minute * 5\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not connect to docker: %s\", err)\n\t}\n\n\t// resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV)\n\tresource, err := pool.RunWithOptions(&dockertest.RunOptions{\n\t\tRepository: dbSetting.ImageName,\n\t\tTag:        dbSetting.ImageVersion,\n\t\tEnv:        dbSetting.ENV,\n\t}, func(config *docker.HostConfig) {\n\t\tconfig.AutoRemove = true\n\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not pull resource: %s\", err)\n\t}\n\n\tconnection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID))\n\tif err := pool.Retry(func() error {\n\t\tdb, err := sql.Open(dbSetting.Driver, connection)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn db.Ping()\n\t}); err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not connect to database: %s\", err)\n\t}\n\treturn connection, func() { _ = pool.Purge(resource) }, nil\n}\n\nfunc initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) {\n\tdataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection}\n\tdbEngine, err = data.NewDB(true, dataConf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"connection to database failed: %s\", err)\n\t}\n\tif err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{\n\t\tLanguage:      \"en_US\",\n\t\tSiteName:      \"ANSWER\",\n\t\tSiteURL:       \"http://127.0.0.1:8080/\",\n\t\tContactEmail:  \"answer@answer.com\",\n\t\tAdminName:     \"admin\",\n\t\tAdminPassword: \"admin\",\n\t\tAdminEmail:    \"answer@answer.com\",\n\t}).InitDB(); err != nil {\n\t\treturn nil, fmt.Errorf(\"migrations init database failed: %s\", err)\n\t}\n\treturn dbEngine, nil\n}\n\nfunc initCache() (newCache cache.Cache, err error) {\n\tnewCache, _, err = data.NewCache(&data.CacheConf{})\n\treturn newCache, err\n}\n"
  },
  {
    "path": "internal/repo/repo_test/revision_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/question\"\n\t\"github.com/apache/answer/internal/repo/revision\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar q = &entity.Question{\n\tID:               \"\",\n\tUserID:           \"1\",\n\tTitle:            \"test\",\n\tOriginalText:     \"test\",\n\tParsedText:       \"test\",\n\tStatus:           entity.QuestionStatusAvailable,\n\tViewCount:        0,\n\tUniqueViewCount:  0,\n\tVoteCount:        0,\n\tAnswerCount:      0,\n\tCollectionCount:  0,\n\tFollowCount:      0,\n\tAcceptedAnswerID: \"\",\n\tLastAnswerID:     \"\",\n\tRevisionID:       \"0\",\n}\n\nfunc getRev(objID, title, content string) *entity.Revision {\n\treturn &entity.Revision{\n\t\tID:       \"\",\n\t\tUserID:   \"1\",\n\t\tObjectID: objID,\n\t\tTitle:    title,\n\t\tContent:  content,\n\t\tLog:      \"add rev\",\n\t}\n}\n\nfunc Test_revisionRepo_AddRevision(t *testing.T) {\n\tvar (\n\t\tuniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)\n\t\trevisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)\n\t\tquestionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo)\n\t)\n\n\t// create question\n\terr := questionRepo.AddQuestion(context.TODO(), q)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, q.ID)\n\n\tcontent, err := json.Marshal(q)\n\trequire.NoError(t, err)\n\t// auto update false\n\trev := getRev(q.ID, q.Title, string(content))\n\terr = revisionRepo.AddRevision(context.TODO(), rev, false)\n\trequire.NoError(t, err)\n\tqr, _, _ := questionRepo.GetQuestion(context.TODO(), q.ID)\n\tassert.NotEqual(t, rev.ID, qr.RevisionID)\n\n\t// auto update false\n\trev = getRev(q.ID, q.Title, string(content))\n\terr = revisionRepo.AddRevision(context.TODO(), rev, true)\n\trequire.NoError(t, err)\n\tqr, _, _ = questionRepo.GetQuestion(context.TODO(), q.ID)\n\tassert.Equal(t, rev.ID, qr.RevisionID)\n\n\t// recovery\n\tt.Cleanup(func() {\n\t\terr = questionRepo.RemoveQuestion(context.TODO(), q.ID)\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_revisionRepo_GetLastRevisionByObjectID(t *testing.T) {\n\tvar (\n\t\tuniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)\n\t\trevisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)\n\t)\n\n\tTest_revisionRepo_AddRevision(t)\n\trev, exists, err := revisionRepo.GetLastRevisionByObjectID(context.TODO(), q.ID)\n\trequire.NoError(t, err)\n\tassert.True(t, exists)\n\tassert.NotNil(t, rev)\n}\n\nfunc Test_revisionRepo_GetRevisionList(t *testing.T) {\n\tvar (\n\t\tuniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)\n\t\trevisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)\n\t)\n\tTest_revisionRepo_AddRevision(t)\n\trevs, err := revisionRepo.GetRevisionList(context.TODO(), &entity.Revision{ObjectID: q.ID})\n\trequire.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(revs), 1)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/siteinfo_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/site_info\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_siteInfoRepo_SaveByType(t *testing.T) {\n\tsiteInfoRepo := site_info.NewSiteInfo(testDataSource)\n\n\tdata := &entity.SiteInfo{Content: \"site_info\", Type: \"test\"}\n\n\terr := siteInfoRepo.SaveByType(context.TODO(), data.Type, data)\n\trequire.NoError(t, err)\n\n\tgot, exist, err := siteInfoRepo.GetByType(context.TODO(), data.Type)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, data.Content, got.Content)\n\n\tdata.Content = \"new site_info\"\n\terr = siteInfoRepo.SaveByType(context.TODO(), data.Type, data)\n\trequire.NoError(t, err)\n\n\tgot, exist, err = siteInfoRepo.GetByType(context.TODO(), data.Type)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, data.Content, got.Content)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/tag_rel_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/repo/unique\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/tag\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\ttagRelOnce     sync.Once\n\ttestTagRelList = []*entity.TagRel{\n\t\t{\n\t\t\tObjectID: \"10010000000000101\",\n\t\t\tTagID:    \"10030000000000101\",\n\t\t\tStatus:   entity.TagRelStatusAvailable,\n\t\t},\n\t\t{\n\t\t\tObjectID: \"10010000000000202\",\n\t\t\tTagID:    \"10030000000000202\",\n\t\t\tStatus:   entity.TagRelStatusAvailable,\n\t\t},\n\t}\n)\n\nfunc addTagRelList() {\n\ttagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\terr := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList)\n\tif err != nil {\n\t\tlog.Fatalf(\"%+v\", err)\n\t}\n}\n\nfunc Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) {\n\ttagRelOnce.Do(addTagRelList)\n\ttagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\trelList, err :=\n\t\ttagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID})\n\trequire.NoError(t, err)\n\tassert.Len(t, relList, 2)\n}\n\nfunc Test_tagListRepo_CountTagRelByTagID(t *testing.T) {\n\ttagRelOnce.Do(addTagRelList)\n\ttagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\tcount, err := tagRelRepo.CountTagRelByTagID(context.TODO(), \"10030000000000101\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), count)\n}\n\nfunc Test_tagListRepo_GetObjectTagRelList(t *testing.T) {\n\ttagRelOnce.Do(addTagRelList)\n\ttagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\trelList, err :=\n\t\ttagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID)\n\trequire.NoError(t, err)\n\tassert.Len(t, relList, 1)\n}\n\nfunc Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) {\n\ttagRelOnce.Do(addTagRelList)\n\ttagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\trelList, err :=\n\t\ttagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID})\n\trequire.NoError(t, err)\n\tassert.Len(t, relList, 2)\n\n\tids := []int64{relList[0].ID, relList[1].ID}\n\terr = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids)\n\trequire.NoError(t, err)\n\n\tcount, err := tagRelRepo.CountTagRelByTagID(context.TODO(), \"10030000000000101\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(0), count)\n\n\t_, exist, err := tagRelRepo.GetObjectTagRelWithoutStatus(context.TODO(), relList[0].ObjectID, relList[0].TagID)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\n\terr = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids, false)\n\trequire.NoError(t, err)\n\n\tcount, err = tagRelRepo.CountTagRelByTagID(context.TODO(), \"10030000000000101\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), count)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/tag_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/tag\"\n\t\"github.com/apache/answer/internal/repo/tag_common\"\n\t\"github.com/apache/answer/internal/repo/unique\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\ttagOnce     sync.Once\n\ttestTagList = []*entity.Tag{\n\t\t{\n\t\t\tSlugName:     \"go\",\n\t\t\tDisplayName:  \"Golang\",\n\t\t\tOriginalText: \"golang\",\n\t\t\tParsedText:   \"<p>golang</p>\",\n\t\t\tStatus:       entity.TagStatusAvailable,\n\t\t},\n\t\t{\n\t\t\tSlugName:     \"js\",\n\t\t\tDisplayName:  \"javascript\",\n\t\t\tOriginalText: \"javascript\",\n\t\t\tParsedText:   \"<p>javascript</p>\",\n\t\t\tStatus:       entity.TagStatusAvailable,\n\t\t},\n\t\t{\n\t\t\tSlugName:     \"go2\",\n\t\t\tDisplayName:  \"Golang2\",\n\t\t\tOriginalText: \"golang2\",\n\t\t\tParsedText:   \"<p>golang2</p>\",\n\t\t\tStatus:       entity.TagStatusAvailable,\n\t\t},\n\t}\n)\n\nfunc addTagList() {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)\n\terr := tagCommonRepo.AddTagList(context.TODO(), testTagList)\n\tif err != nil {\n\t\tlog.Fatalf(\"%+v\", err)\n\t}\n}\n\nfunc Test_tagRepo_GetTagByID(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)\n}\n\nfunc Test_tagRepo_GetTagBySlugName(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)\n}\n\nfunc Test_tagRepo_GetTagList(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTags, err := tagRepo.GetTagList(context.TODO(), &entity.Tag{ID: testTagList[0].ID})\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)\n}\n\nfunc Test_tagRepo_GetTagListByIDs(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)\n}\n\nfunc Test_tagRepo_GetTagListByName(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false, false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)\n}\n\nfunc Test_tagRepo_GetTagListByNames(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)\n}\n\nfunc Test_tagRepo_GetTagPage(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)\n}\n\nfunc Test_tagRepo_RemoveTag(t *testing.T) {\n\ttagOnce.Do(addTagList)\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\ttagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)\n\terr := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID)\n\trequire.NoError(t, err)\n\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\t_, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)\n\trequire.NoError(t, err)\n\tassert.False(t, exist)\n}\n\nfunc Test_tagRepo_UpdateTag(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\ttagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)\n\n\ttestTagList[0].DisplayName = \"golang\"\n\terr := tagRepo.UpdateTag(context.TODO(), testTagList[0])\n\trequire.NoError(t, err)\n\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)\n}\n\nfunc Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\ttestTagList[0].DisplayName = \"golang\"\n\terr := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)\n\trequire.NoError(t, err)\n\n\tgotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, 100, gotTag.QuestionCount)\n}\n\nfunc Test_tagRepo_UpdateTagSynonym(t *testing.T) {\n\tuniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)\n\ttagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)\n\n\ttestTagList[0].DisplayName = \"golang\"\n\terr := tagRepo.UpdateTag(context.TODO(), testTagList[0])\n\trequire.NoError(t, err)\n\n\terr = tagRepo.UpdateTagSynonym(context.TODO(), []string{testTagList[2].SlugName},\n\t\tconverter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName)\n\trequire.NoError(t, err)\n\n\ttagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))\n\n\tgotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true)\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, testTagList[0].ID, fmt.Sprintf(\"%d\", gotTag.MainTagID))\n}\n"
  },
  {
    "path": "internal/repo/repo_test/user_backyard_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/auth\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_userAdminRepo_GetUserInfo(t *testing.T) {\n\tuserAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))\n\tgot, exist, err := userAdminRepo.GetUserInfo(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, \"1\", got.ID)\n}\n\nfunc Test_userAdminRepo_GetUserPage(t *testing.T) {\n\tuserAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))\n\tgot, total, err := userAdminRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: \"admin\"}, \"\", false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), total)\n\tassert.Equal(t, \"1\", got[0].ID)\n}\n\nfunc Test_userAdminRepo_UpdateUserStatus(t *testing.T) {\n\tuserAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))\n\tgot, exist, err := userAdminRepo.GetUserInfo(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, entity.UserStatusAvailable, got.Status)\n\n\terr = userAdminRepo.UpdateUserStatus(context.TODO(), \"1\", entity.UserStatusSuspended, entity.EmailStatusAvailable,\n\t\t\"admin@admin.com\", time.Now().Add(time.Minute*5))\n\trequire.NoError(t, err)\n\n\tgot, exist, err = userAdminRepo.GetUserInfo(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, entity.UserStatusSuspended, got.Status)\n\n\terr = userAdminRepo.UpdateUserStatus(context.TODO(), \"1\", entity.UserStatusAvailable, entity.EmailStatusAvailable,\n\t\t\"admin@admin.com\", time.Time{})\n\trequire.NoError(t, err)\n\n\tgot, exist, err = userAdminRepo.GetUserInfo(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, entity.UserStatusAvailable, got.Status)\n}\n"
  },
  {
    "path": "internal/repo/repo_test/user_repo_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage repo_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/user\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_userRepo_AddUser(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\tuserInfo := &entity.User{\n\t\tUsername:    \"answer\",\n\t\tPass:        \"answer\",\n\t\tEMail:       \"answer@example.com\",\n\t\tMailStatus:  entity.EmailStatusAvailable,\n\t\tStatus:      entity.UserStatusAvailable,\n\t\tDisplayName: \"answer\",\n\t\tIsAdmin:     false,\n\t}\n\terr := userRepo.AddUser(context.TODO(), userInfo)\n\trequire.NoError(t, err)\n}\n\nfunc Test_userRepo_BatchGetByID(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\tgot, err := userRepo.BatchGetByID(context.TODO(), []string{\"1\"})\n\trequire.NoError(t, err)\n\tassert.Len(t, got, 1)\n\tassert.Equal(t, \"admin\", got[0].Username)\n}\n\nfunc Test_userRepo_GetByEmail(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\tgot, exist, err := userRepo.GetByEmail(context.TODO(), \"admin@admin.com\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, \"admin\", got.Username)\n}\n\nfunc Test_userRepo_GetByUserID(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\tgot, exist, err := userRepo.GetByUserID(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, \"admin\", got.Username)\n}\n\nfunc Test_userRepo_GetByUsername(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\tgot, exist, err := userRepo.GetByUsername(context.TODO(), \"admin\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, \"admin\", got.Username)\n}\n\nfunc Test_userRepo_IncreaseAnswerCount(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.IncreaseAnswerCount(context.TODO(), \"1\", 1)\n\trequire.NoError(t, err)\n\n\tgot, exist, err := userRepo.GetByUserID(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, 1, got.AnswerCount)\n}\n\nfunc Test_userRepo_IncreaseQuestionCount(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.IncreaseQuestionCount(context.TODO(), \"1\", 1)\n\trequire.NoError(t, err)\n\n\tgot, exist, err := userRepo.GetByUserID(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, 1, got.AnswerCount)\n}\n\nfunc Test_userRepo_UpdateEmail(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdateEmail(context.TODO(), \"1\", \"admin@admin.com\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_userRepo_UpdateEmailStatus(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdateEmailStatus(context.TODO(), \"1\", entity.EmailStatusToBeVerified)\n\trequire.NoError(t, err)\n}\n\nfunc Test_userRepo_UpdateInfo(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: \"1\", Bio: \"test\"})\n\trequire.NoError(t, err)\n\n\tgot, exist, err := userRepo.GetByUserID(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n\tassert.True(t, exist)\n\tassert.Equal(t, \"test\", got.Bio)\n}\n\nfunc Test_userRepo_UpdateLastLoginDate(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdateLastLoginDate(context.TODO(), \"1\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_userRepo_UpdateNoticeStatus(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdateNoticeStatus(context.TODO(), \"1\", 1)\n\trequire.NoError(t, err)\n}\n\nfunc Test_userRepo_UpdatePass(t *testing.T) {\n\tuserRepo := user.NewUserRepo(testDataSource)\n\terr := userRepo.UpdatePass(context.TODO(), \"1\", \"admin\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/repo/report/report_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage report\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/report_common\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// reportRepo report repository\ntype reportRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewReportRepo new repository\nfunc NewReportRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) report_common.ReportRepo {\n\treturn &reportRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddReport add report\nfunc (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err error) {\n\treport.ID, err = rr.uniqueIDRepo.GenUniqueIDStr(ctx, report.TableName())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = rr.data.DB.Context(ctx).Insert(report)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetReportListPage get report list page\nfunc (rr *reportRepo) GetReportListPage(ctx context.Context, dto *schema.GetReportListPageDTO) (\n\treports []*entity.Report, total int64, err error) {\n\tcond := &entity.Report{}\n\tcond.Status = dto.Status\n\tsession := rr.data.DB.Context(ctx).Desc(\"updated_at\")\n\ttotal, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetByID get report by ID\nfunc (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) {\n\treport = &entity.Report{}\n\texist, err = rr.data.DB.Context(ctx).ID(id).Get(report)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateStatus update report status by ID\nfunc (rr *reportRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) {\n\t_, err = rr.data.DB.Context(ctx).ID(id).Update(&entity.Report{Status: status})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {\n\tlist := make([]*entity.Report, 0)\n\tcount, err = rr.data.DB.Context(ctx).Where(\"status =?\", entity.ReportStatusPending).FindAndCount(&list)\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/review/review_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage review\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// reviewRepo review repository\ntype reviewRepo struct {\n\tdata *data.Data\n}\n\n// NewReviewRepo new repository\nfunc NewReviewRepo(data *data.Data) review.ReviewRepo {\n\treturn &reviewRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddReview add review\nfunc (cr *reviewRepo) AddReview(ctx context.Context, review *entity.Review) (err error) {\n\t_, err = cr.data.DB.Context(ctx).Insert(review)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateReviewStatus update review status\nfunc (cr *reviewRepo) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) {\n\t_, err = cr.data.DB.Context(ctx).ID(reviewID).Update(&entity.Review{\n\t\tReviewerUserID: reviewerUserID, Status: status})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetReview get review one\nfunc (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) (\n\treview *entity.Review, exist bool, err error) {\n\treview = &entity.Review{}\n\texist, err = cr.data.DB.Context(ctx).ID(reviewID).Get(review)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetReviewByObject get review by object\nfunc (cr *reviewRepo) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) {\n\treview = &entity.Review{}\n\texist, err = cr.data.DB.Context(ctx).Desc(\"id\").Where(\"object_id = ?\", objectID).Get(review)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetReviewCount get review count\nfunc (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) {\n\tcount, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetReviewPage get review page\nfunc (cr *reviewRepo) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (\n\treviewList []*entity.Review, total int64, err error) {\n\tsession := cr.data.DB.Context(ctx).Asc(\"created_at\")\n\treviewList = make([]*entity.Review, 0)\n\ttotal, err = pager.Help(page, pageSize, &reviewList, cond, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/revision/revision_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage revision\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// revisionRepo revision repository\ntype revisionRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewRevisionRepo new repository\nfunc NewRevisionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) revision.RevisionRepo {\n\treturn &revisionRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddRevision add revision\n// autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated,\n// if not need auto update object.revision_id, it must be false.\n// example: user can edit the object, but need audit, the revision_id will be updated when admin approved\nfunc (rr *revisionRepo) AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) {\n\tobjectTypeNumber, err := obj.GetObjectTypeNumberByObjectID(revision.ObjectID)\n\tif err != nil {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\trevision.ObjectType = objectTypeNumber\n\tif !rr.allowRecord(revision.ObjectType) {\n\t\treturn nil\n\t}\n\t_, err = rr.data.DB.Transaction(func(session *xorm.Session) (any, error) {\n\t\tsession = session.Context(ctx)\n\t\t_, err = session.Insert(revision)\n\t\tif err != nil {\n\t\t\t_ = session.Rollback()\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif autoUpdateRevisionID {\n\t\t\terr = rr.UpdateObjectRevisionId(ctx, revision, session)\n\t\t\tif err != nil {\n\t\t\t\t_ = session.Rollback()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn nil, nil\n\t})\n\n\treturn err\n}\n\n// UpdateObjectRevisionId updates the object.revision_id field\nfunc (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) {\n\ttableName, err := obj.GetObjectTypeStrByObjectID(revision.ObjectID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\t_, err = session.Table(tableName).Where(\"id = ?\", revision.ObjectID).Cols(\"`revision_id`\").Update(struct {\n\t\tRevisionID string `xorm:\"revision_id\"`\n\t}{\n\t\tRevisionID: revision.ID,\n\t})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// UpdateStatus update revision status\nfunc (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) {\n\tif id == \"\" {\n\t\treturn nil\n\t}\n\tvar data entity.Revision\n\tdata.ID = id\n\tdata.Status = status\n\tdata.ReviewUserID = converter.StringToInt64(reviewUserID)\n\t_, err = rr.data.DB.Context(ctx).Where(\"id =?\", id).Cols(\"status\", \"review_user_id\").Update(&data)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// GetRevision get revision one\nfunc (rr *revisionRepo) GetRevision(ctx context.Context, id string) (\n\trevision *entity.Revision, exist bool, err error,\n) {\n\trevision = &entity.Revision{}\n\texist, err = rr.data.DB.Context(ctx).ID(id).Get(revision)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetRevisionByID get object's last revision by object TagID\nfunc (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) (\n\trevision *entity.Revision, exist bool, err error) {\n\trevision = &entity.Revision{}\n\texist, err = rr.data.DB.Context(ctx).Where(\"id = ?\", revisionID).Get(revision)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (\n\trevision *entity.Revision, exist bool, err error) {\n\trevision = &entity.Revision{}\n\texist, err = rr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).And(\"status = ?\", entity.RevisionUnreviewedStatus).Get(revision)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetLastRevisionByObjectID get object's last revision by object TagID\nfunc (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) (\n\trevision *entity.Revision, exist bool, err error,\n) {\n\trevision = &entity.Revision{}\n\texist, err = rr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).Desc(\"created_at\").Get(revision)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetLastRevisionByFileURL get object's last revision by file url\nfunc (rr *revisionRepo) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) {\n\trevision = &entity.Revision{}\n\texist, err = rr.data.DB.Context(ctx).Where(\"content LIKE ?\", \"%\"+fileURL+\"%\").Desc(\"created_at\").Get(revision)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetRevisionList get revision list all\nfunc (rr *revisionRepo) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) {\n\trevisionList = []entity.Revision{}\n\terr = rr.data.DB.Context(ctx).Where(builder.Eq{\n\t\t\"object_id\": revision.ObjectID,\n\t}).OrderBy(\"created_at DESC\").Find(&revisionList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// allowRecord check the object type can record revision or not\nfunc (rr *revisionRepo) allowRecord(objectType int) (ok bool) {\n\tswitch objectType {\n\tcase constant.ObjectTypeStrMapping[\"question\"]:\n\t\treturn true\n\tcase constant.ObjectTypeStrMapping[\"answer\"]:\n\t\treturn true\n\tcase constant.ObjectTypeStrMapping[\"tag\"]:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// GetUnreviewedRevisionPage get unreviewed revision page\nfunc (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int,\n\tobjectTypeList []int) (revisionList []*entity.Revision, total int64, err error) {\n\trevisionList = make([]*entity.Revision, 0)\n\tif len(objectTypeList) == 0 {\n\t\treturn revisionList, 0, nil\n\t}\n\tsession := rr.data.DB.Context(ctx)\n\tsession = session.And(\"status = ?\", entity.RevisionUnreviewedStatus)\n\tsession = session.In(\"object_type\", objectTypeList)\n\tsession = session.OrderBy(\"created_at asc\")\n\n\ttotal, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// CountUnreviewedRevision get unreviewed revision count\nfunc (rr *revisionRepo) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) {\n\tif len(objectTypeList) == 0 {\n\t\treturn 0, nil\n\t}\n\tsession := rr.data.DB.Context(ctx)\n\tsession = session.And(\"status = ?\", entity.RevisionUnreviewedStatus)\n\tsession = session.In(\"object_type\", objectTypeList)\n\tcount, err = session.Count(&entity.Revision{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/role/power_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// powerRepo power repository\ntype powerRepo struct {\n\tdata *data.Data\n}\n\n// NewPowerRepo new repository\nfunc NewPowerRepo(data *data.Data) role.PowerRepo {\n\treturn &powerRepo{\n\t\tdata: data,\n\t}\n}\n\n// GetPowerList get  list all\nfunc (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) {\n\tpowerList = make([]*entity.Power, 0)\n\terr = pr.data.DB.Context(ctx).Find(&powerList, power)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/role/role_power_rel_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n)\n\n// rolePowerRelRepo rolePowerRel repository\ntype rolePowerRelRepo struct {\n\tdata *data.Data\n}\n\n// NewRolePowerRelRepo new repository\nfunc NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo {\n\treturn &rolePowerRelRepo{\n\t\tdata: data,\n\t}\n}\n\n// GetRolePowerTypeList get role power type list\nfunc (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) {\n\tpowers = make([]string, 0)\n\terr = rr.data.DB.Context(ctx).Table(\"role_power_rel\").\n\t\tCols(\"power_type\").Where(builder.Eq{\"role_id\": roleID}).Find(&powers)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/role/role_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\tservice \"github.com/apache/answer/internal/service/role\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// roleRepo role repository\ntype roleRepo struct {\n\tdata *data.Data\n}\n\n// NewRoleRepo new repository\nfunc NewRoleRepo(data *data.Data) service.RoleRepo {\n\treturn &roleRepo{\n\t\tdata: data,\n\t}\n}\n\n// GetRoleAllList get role list all\nfunc (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) {\n\troleList = make([]*entity.Role, 0)\n\terr = rr.data.DB.Context(ctx).Find(&roleList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetRoleAllMapping get role all mapping\nfunc (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {\n\troleList, err := rr.GetRoleAllList(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troleMapping = make(map[int]*entity.Role, 0)\n\tfor _, role := range roleList {\n\t\troleMapping[role.ID] = role\n\t}\n\treturn roleMapping, nil\n}\n"
  },
  {
    "path": "internal/repo/role/user_role_rel_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// userRoleRelRepo userRoleRel repository\ntype userRoleRelRepo struct {\n\tdata *data.Data\n}\n\n// NewUserRoleRelRepo new repository\nfunc NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo {\n\treturn &userRoleRelRepo{\n\t\tdata: data,\n\t}\n}\n\n// SaveUserRoleRel save user role rel\nfunc (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) {\n\t_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {\n\t\tsession = session.Context(ctx)\n\t\titem := &entity.UserRoleRel{UserID: userID}\n\t\texist, err := session.Get(item)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\titem.RoleID = roleID\n\t\t\t_, err = session.ID(item.ID).Update(item)\n\t\t} else {\n\t\t\t_, err = session.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUserRoleRelList get user role all\nfunc (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) (\n\tuserRoleRelList []*entity.UserRoleRel, err error) {\n\tuserRoleRelList = make([]*entity.UserRoleRel, 0)\n\terr = ur.data.DB.Context(ctx).In(\"user_id\", userIDs).Find(&userRoleRelList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUserRoleRelListByRoleID get user role all by role id\nfunc (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (\n\tuserRoleRelList []*entity.UserRoleRel, err error) {\n\tuserRoleRelList = make([]*entity.UserRoleRel, 0)\n\terr = ur.data.DB.Context(ctx).In(\"role_id\", roleIDs).Find(&userRoleRelList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUserRoleRel get user role\nfunc (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) (\n\trolePowerRel *entity.UserRoleRel, exist bool, err error) {\n\trolePowerRel = &entity.UserRoleRel{}\n\texist, err = ur.data.DB.Context(ctx).Where(builder.Eq{\"user_id\": userID}).Get(rolePowerRel)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/search_common/search_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage search_common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/plugin\"\n\n\t\"github.com/apache/answer/pkg/htmltext\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/search_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n)\n\nvar (\n\tqFields = []string{\n\t\t\"`question`.`id`\",\n\t\t\"`question`.`id` as `question_id`\",\n\t\t\"`title`\",\n\t\t\"`parsed_text`\",\n\t\t\"`question`.`created_at` as `created_at`\",\n\t\t\"`user_id`\",\n\t\t\"`vote_count`\",\n\t\t\"`answer_count`\",\n\t\t\"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`\",\n\t\t\"`question`.`status` as `status`\",\n\t\t\"`post_update_time`\",\n\t}\n\taFields = []string{\n\t\t\"`answer`.`id` as `id`\",\n\t\t\"`question_id`\",\n\t\t\"`question`.`title` as `title`\",\n\t\t\"`answer`.`parsed_text` as `parsed_text`\",\n\t\t\"`answer`.`created_at` as `created_at`\",\n\t\t\"`answer`.`user_id` as `user_id`\",\n\t\t\"`answer`.`vote_count` as `vote_count`\",\n\t\t\"0 as `answer_count`\",\n\t\t\"`adopted` as `accepted`\",\n\t\t\"`answer`.`status` as `status`\",\n\t\t\"`answer`.`created_at` as `post_update_time`\",\n\t}\n)\n\n// searchRepo tag repository\ntype searchRepo struct {\n\tdata         *data.Data\n\tuserCommon   *usercommon.UserCommon\n\tuniqueIDRepo unique.UniqueIDRepo\n\ttagCommon    *tagcommon.TagCommonService\n}\n\n// NewSearchRepo new repository\nfunc NewSearchRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n\tuserCommon *usercommon.UserCommon,\n\ttagCommon *tagcommon.TagCommonService,\n) search_common.SearchRepo {\n\treturn &searchRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t\tuserCommon:   userCommon,\n\t\ttagCommon:    tagCommon,\n\t}\n}\n\n// SearchContents search question and answer data\nfunc (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {\n\twords = filterWords(words)\n\n\tvar (\n\t\tb     *builder.Builder\n\t\tub    *builder.Builder\n\t\tqfs   = qFields\n\t\tafs   = aFields\n\t\targsQ = []any{}\n\t\targsA = []any{}\n\t)\n\n\tif order == \"relevance\" {\n\t\tif len(words) > 0 {\n\t\t\tqfs, argsQ = addRelevanceField([]string{\"title\", \"original_text\"}, words, qfs)\n\t\t\tafs, argsA = addRelevanceField([]string{\"`answer`.`original_text`\"}, words, afs)\n\t\t} else {\n\t\t\torder = \"newest\"\n\t\t}\n\t}\n\n\tb = builder.MySQL().Select(qfs...).From(\"`question`\")\n\tub = builder.MySQL().Select(afs...).From(\"`answer`\").\n\t\tLeftJoin(\"`question`\", \"`question`.id = `answer`.question_id\")\n\n\tb.Where(builder.Lt{\"`question`.`status`\": entity.QuestionStatusDeleted}).\n\t\tAnd(builder.Eq{\"`question`.`show`\": entity.QuestionShow})\n\tub.Where(builder.Lt{\"`question`.`status`\": entity.QuestionStatusDeleted}).\n\t\tAnd(builder.Lt{\"`answer`.`status`\": entity.AnswerStatusDeleted}).\n\t\tAnd(builder.Eq{\"`question`.`show`\": entity.QuestionShow})\n\n\targsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)\n\targsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)\n\n\tlikeConQ := builder.NewCond()\n\tlikeConA := builder.NewCond()\n\tfor _, word := range words {\n\t\tlikeConQ = likeConQ.Or(builder.Like{\"title\", word}).\n\t\t\tOr(builder.Like{\"original_text\", word})\n\t\targsQ = append(argsQ, \"%\"+word+\"%\")\n\t\targsQ = append(argsQ, \"%\"+word+\"%\")\n\n\t\tlikeConA = likeConA.Or(builder.Like{\"`answer`.original_text\", word})\n\t\targsA = append(argsA, \"%\"+word+\"%\")\n\t}\n\n\tb.Where(likeConQ)\n\tub.Where(likeConA)\n\n\t// check tag\n\tfor ti, tagID := range tagIDs {\n\t\tast := \"tag_rel\" + strconv.Itoa(ti)\n\t\tb.Join(\"INNER\", \"tag_rel as \"+ast, \"question.id = \"+ast+\".object_id\").\n\t\t\tAnd(builder.Eq{\n\t\t\t\tast + \".status\": entity.TagRelStatusAvailable,\n\t\t\t}).\n\t\t\tAnd(builder.In(ast+\".tag_id\", tagID))\n\t\tub.Join(\"INNER\", \"tag_rel as \"+ast, \"question_id = \"+ast+\".object_id\").\n\t\t\tAnd(builder.Eq{\n\t\t\t\tast + \".status\": entity.TagRelStatusAvailable,\n\t\t\t}).\n\t\t\tAnd(builder.In(ast+\".tag_id\", tagID))\n\t\targsQ = append(argsQ, entity.TagRelStatusAvailable)\n\t\targsA = append(argsA, entity.TagRelStatusAvailable)\n\t\tfor _, t := range tagID {\n\t\t\targsQ = append(argsQ, t)\n\t\t\targsA = append(argsA, t)\n\t\t}\n\t}\n\n\t// check user\n\tif userID != \"\" {\n\t\tb.Where(builder.Eq{\"question.user_id\": userID})\n\t\tub.Where(builder.Eq{\"answer.user_id\": userID})\n\t\targsQ = append(argsQ, userID)\n\t\targsA = append(argsA, userID)\n\t}\n\n\t// check vote\n\tif votes == 0 {\n\t\tb.Where(builder.Eq{\"question.vote_count\": votes})\n\t\tub.Where(builder.Eq{\"answer.vote_count\": votes})\n\t\targsQ = append(argsQ, votes)\n\t\targsA = append(argsA, votes)\n\t} else if votes > 0 {\n\t\tb.Where(builder.Gte{\"question.vote_count\": votes})\n\t\tub.Where(builder.Gte{\"answer.vote_count\": votes})\n\t\targsQ = append(argsQ, votes)\n\t\targsA = append(argsA, votes)\n\t}\n\n\t// b = b.Union(\"all\", ub)\n\tubSQL, _, err := ub.ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\tbSQL, _, err := b.ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\tsql := fmt.Sprintf(\"(%s UNION ALL %s)\", bSQL, ubSQL)\n\n\tcountSQL, _, err := builder.MySQL().Select(\"count(*) total\").From(sql, \"c\").ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tstartNum := (page - 1) * pageSize\n\tquerySQL, _, err := builder.MySQL().Select(\"*\").From(sql, \"t\").OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tqueryArgs := []any{}\n\tcountArgs := []any{}\n\n\tqueryArgs = append(queryArgs, querySQL)\n\tqueryArgs = append(queryArgs, argsQ...)\n\tqueryArgs = append(queryArgs, argsA...)\n\n\tcountArgs = append(countArgs, countSQL)\n\tcountArgs = append(countArgs, argsQ...)\n\tcountArgs = append(countArgs, argsA...)\n\n\tres, err := sr.data.DB.Context(ctx).Query(queryArgs...)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttr, err := sr.data.DB.Context(ctx).Query(countArgs...)\n\tif len(tr) != 0 {\n\t\ttotal = converter.StringToInt64(string(tr[0][\"total\"]))\n\t}\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t} else {\n\t\tresp, err = sr.parseResult(ctx, res, words)\n\t\treturn\n\t}\n}\n\n// SearchQuestions search question data\nfunc (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {\n\twords = filterWords(words)\n\tvar (\n\t\tqfs  = qFields\n\t\targs = []any{}\n\t)\n\tif order == \"relevance\" {\n\t\tif len(words) > 0 {\n\t\t\tqfs, args = addRelevanceField([]string{\"title\", \"original_text\"}, words, qfs)\n\t\t} else {\n\t\t\torder = \"newest\"\n\t\t}\n\t}\n\n\tb := builder.MySQL().Select(qfs...).From(\"question\")\n\n\tb.Where(builder.Lt{\"`question`.`status`\": entity.QuestionStatusDeleted}).And(builder.Eq{\"`question`.`show`\": entity.QuestionShow})\n\targs = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)\n\n\tlikeConQ := builder.NewCond()\n\tfor _, word := range words {\n\t\tlikeConQ = likeConQ.Or(builder.Like{\"title\", word}).\n\t\t\tOr(builder.Like{\"original_text\", word})\n\t\targs = append(args, \"%\"+word+\"%\")\n\t\targs = append(args, \"%\"+word+\"%\")\n\t}\n\tb.Where(likeConQ)\n\n\t// check tag\n\tfor ti, tagID := range tagIDs {\n\t\tast := \"tag_rel\" + strconv.Itoa(ti)\n\t\tb.Join(\"INNER\", \"tag_rel as \"+ast, \"question.id = \"+ast+\".object_id\").\n\t\t\tAnd(builder.Eq{\n\t\t\t\tast + \".status\": entity.TagRelStatusAvailable,\n\t\t\t}).\n\t\t\tAnd(builder.In(ast+\".tag_id\", tagID))\n\t\targs = append(args, entity.TagRelStatusAvailable)\n\t\tfor _, t := range tagID {\n\t\t\targs = append(args, t)\n\t\t}\n\t}\n\n\t// check need filter has not accepted\n\tif notAccepted {\n\t\tb.And(builder.Eq{\"accepted_answer_id\": 0})\n\t\targs = append(args, 0)\n\t}\n\n\t// check views\n\tif views > -1 {\n\t\tb.And(builder.Gte{\"view_count\": views})\n\t\targs = append(args, views)\n\t}\n\n\t// check answers\n\tif answers == 0 {\n\t\tb.And(builder.Eq{\"answer_count\": answers})\n\t\targs = append(args, answers)\n\t} else if answers > 0 {\n\t\tb.And(builder.Gte{\"answer_count\": answers})\n\t\targs = append(args, answers)\n\t}\n\n\tif answers == 0 {\n\t\tb.And(builder.Eq{\"answer_count\": 0})\n\t\targs = append(args, 0)\n\t} else if answers > 0 {\n\t\tb.And(builder.Gte{\"answer_count\": answers})\n\t\targs = append(args, answers)\n\t}\n\n\tqueryArgs := []any{}\n\tcountArgs := []any{}\n\n\tcountSQL, _, err := builder.MySQL().Select(\"count(*) total\").From(b, \"c\").ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tstartNum := (page - 1) * pageSize\n\tquerySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\tqueryArgs = append(queryArgs, querySQL)\n\tqueryArgs = append(queryArgs, args...)\n\n\tcountArgs = append(countArgs, countSQL)\n\tcountArgs = append(countArgs, args...)\n\n\tres, err := sr.data.DB.Context(ctx).Query(queryArgs...)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttr, err := sr.data.DB.Context(ctx).Query(countArgs...)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif len(tr) != 0 {\n\t\ttotal = converter.StringToInt64(string(tr[0][\"total\"]))\n\t}\n\tresp, err = sr.parseResult(ctx, res, words)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// SearchAnswers search answer data\nfunc (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {\n\twords = filterWords(words)\n\n\tvar (\n\t\tafs  = aFields\n\t\targs = []any{}\n\t)\n\tif order == \"relevance\" {\n\t\tif len(words) > 0 {\n\t\t\tafs, args = addRelevanceField([]string{\"`answer`.`original_text`\"}, words, afs)\n\t\t} else {\n\t\t\torder = \"newest\"\n\t\t}\n\t}\n\n\tb := builder.MySQL().Select(afs...).From(\"`answer`\").\n\t\tLeftJoin(\"`question`\", \"`question`.id = `answer`.question_id\")\n\n\tb.Where(builder.Lt{\"`question`.`status`\": entity.QuestionStatusDeleted}).\n\t\tAnd(builder.Lt{\"`answer`.`status`\": entity.AnswerStatusDeleted}).And(builder.Eq{\"`question`.`show`\": entity.QuestionShow})\n\targs = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)\n\n\tlikeConA := builder.NewCond()\n\tfor _, word := range words {\n\t\tlikeConA = likeConA.Or(builder.Like{\"`answer`.original_text\", word})\n\t\targs = append(args, \"%\"+word+\"%\")\n\t}\n\n\tb.Where(likeConA)\n\n\t// check tag\n\tfor ti, tagID := range tagIDs {\n\t\tast := \"tag_rel\" + strconv.Itoa(ti)\n\t\tb.Join(\"INNER\", \"tag_rel as \"+ast, \"question_id = \"+ast+\".object_id\").\n\t\t\tAnd(builder.Eq{\n\t\t\t\tast + \".status\": entity.TagRelStatusAvailable,\n\t\t\t}).\n\t\t\tAnd(builder.In(ast+\".tag_id\", tagID))\n\t\targs = append(args, entity.TagRelStatusAvailable)\n\t\tfor _, t := range tagID {\n\t\t\targs = append(args, t)\n\t\t}\n\t}\n\n\t// check limit accepted\n\tif accepted {\n\t\tb.Where(builder.Eq{\"adopted\": schema.AnswerAcceptedEnable})\n\t\targs = append(args, schema.AnswerAcceptedEnable)\n\t}\n\n\t// check question id\n\tif questionID != \"\" {\n\t\tb.Where(builder.Eq{\"question_id\": questionID})\n\t\targs = append(args, questionID)\n\t}\n\n\tqueryArgs := []any{}\n\tcountArgs := []any{}\n\n\tcountSQL, _, err := builder.MySQL().Select(\"count(*) total\").From(b, \"c\").ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tstartNum := (page - 1) * pageSize\n\tquerySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tqueryArgs = append(queryArgs, querySQL)\n\tqueryArgs = append(queryArgs, args...)\n\n\tcountArgs = append(countArgs, countSQL)\n\tcountArgs = append(countArgs, args...)\n\n\tres, err := sr.data.DB.Context(ctx).Query(queryArgs...)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttr, err := sr.data.DB.Context(ctx).Query(countArgs...)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttotal = converter.StringToInt64(string(tr[0][\"total\"]))\n\tresp, err = sr.parseResult(ctx, res, words)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (sr *searchRepo) parseOrder(_ context.Context, order string) (res string) {\n\tswitch order {\n\tcase \"newest\":\n\t\tres = \"created_at desc\"\n\tcase \"active\":\n\t\tres = \"post_update_time desc\"\n\tcase \"score\":\n\t\tres = \"vote_count desc\"\n\tcase \"relevance\":\n\t\tres = \"relevance desc\"\n\tdefault:\n\t\tres = \"created_at desc\"\n\t}\n\treturn\n}\n\n// ParseSearchPluginResult parse search plugin result\nfunc (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) {\n\tvar (\n\t\tqres []map[string][]byte\n\t\tres  = make([]map[string][]byte, 0)\n\t\tb    *builder.Builder\n\t)\n\tfor _, r := range sres {\n\t\tswitch r.Type {\n\t\tcase \"question\":\n\t\t\tb = builder.MySQL().Select(qFields...).From(\"question\").Where(builder.Eq{\"id\": r.ID}).\n\t\t\t\tAnd(builder.Lt{\"`status`\": entity.QuestionStatusDeleted})\n\t\tcase \"answer\":\n\t\t\tb = builder.MySQL().Select(aFields...).From(\"answer\").LeftJoin(\"`question`\", \"`question`.`id` = `answer`.`question_id`\").\n\t\t\t\tWhere(builder.Eq{\"`answer`.`id`\": r.ID}).\n\t\t\t\tAnd(builder.Lt{\"`question`.`status`\": entity.QuestionStatusDeleted}).\n\t\t\t\tAnd(builder.Lt{\"`answer`.`status`\": entity.AnswerStatusDeleted}).And(builder.Eq{\"`question`.`show`\": entity.QuestionShow})\n\t\t}\n\t\tqres, err = sr.data.DB.Context(ctx).Query(b)\n\t\tif err != nil || len(qres) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tres = append(res, qres[0])\n\t}\n\treturn sr.parseResult(ctx, res, words)\n}\n\n// parseResult parse search result, return the data structure\nfunc (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte, words []string) (resp []*schema.SearchResult, err error) {\n\tquestionIDs := make([]string, 0)\n\tuserIDs := make([]string, 0)\n\tresultList := make([]*schema.SearchResult, 0)\n\tfor _, r := range res {\n\t\tquestionIDs = append(questionIDs, string(r[\"question_id\"]))\n\t\tuserIDs = append(userIDs, string(r[\"user_id\"]))\n\t\ttp, _ := time.ParseInLocation(\"2006-01-02 15:04:05\", string(r[\"created_at\"]), time.Local)\n\n\t\tvar ID = string(r[\"id\"])\n\t\tvar QuestionID = string(r[\"question_id\"])\n\t\tif handler.GetEnableShortID(ctx) {\n\t\t\tID = uid.EnShortID(ID)\n\t\t\tQuestionID = uid.EnShortID(QuestionID)\n\t\t}\n\n\t\tobject := &schema.SearchObject{\n\t\t\tID:              ID,\n\t\t\tQuestionID:      QuestionID,\n\t\t\tTitle:           string(r[\"title\"]),\n\t\t\tUrlTitle:        htmltext.UrlTitle(string(r[\"title\"])),\n\t\t\tExcerpt:         htmltext.FetchMatchedExcerpt(string(r[\"parsed_text\"]), words, \"...\", 100),\n\t\t\tCreatedAtParsed: tp.Unix(),\n\t\t\tUserInfo: &schema.SearchObjectUser{\n\t\t\t\tID: string(r[\"user_id\"]),\n\t\t\t},\n\t\t\tTags:        make([]*schema.TagResp, 0),\n\t\t\tVoteCount:   converter.StringToInt(string(r[\"vote_count\"])),\n\t\t\tAccepted:    string(r[\"accepted\"]) == \"2\",\n\t\t\tAnswerCount: converter.StringToInt(string(r[\"answer_count\"])),\n\t\t}\n\n\t\tobjectKey, err := obj.GetObjectTypeStrByObjectID(string(r[\"id\"]))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch objectKey {\n\t\tcase \"question\":\n\t\t\tfor k, v := range entity.AdminQuestionSearchStatus {\n\t\t\t\tif v == converter.StringToInt(string(r[\"status\"])) {\n\t\t\t\t\tobject.StatusStr = k\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"answer\":\n\t\t\tfor k, v := range entity.AdminAnswerSearchStatus {\n\t\t\t\tif v == converter.StringToInt(string(r[\"status\"])) {\n\t\t\t\t\tobject.StatusStr = k\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresultList = append(resultList, &schema.SearchResult{\n\t\t\tObjectType: objectKey,\n\t\t\tObject:     object,\n\t\t})\n\t}\n\n\ttagsMap, err := sr.tagCommon.BatchGetObjectTag(ctx, questionIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserInfoMap, err := sr.userCommon.BatchUserBasicInfoByID(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, item := range resultList {\n\t\ttags, ok := tagsMap[item.Object.QuestionID]\n\t\tif ok {\n\t\t\titem.Object.Tags = tags\n\t\t}\n\t\tif userInfo := userInfoMap[item.Object.UserInfo.ID]; userInfo != nil {\n\t\t\titem.Object.UserInfo.Username = userInfo.Username\n\t\t\titem.Object.UserInfo.DisplayName = userInfo.DisplayName\n\t\t\titem.Object.UserInfo.Rank = userInfo.Rank\n\t\t\titem.Object.UserInfo.Status = userInfo.Status\n\t\t}\n\t}\n\treturn resultList, nil\n}\n\nfunc addRelevanceField(searchFields, words, fields []string) (res []string, args []any) {\n\trelevanceRes := []string{}\n\targs = []any{}\n\n\tfor _, searchField := range searchFields {\n\t\tvar (\n\t\t\trelevance    = \"(LENGTH(\" + searchField + \") - LENGTH(%s))\"\n\t\t\treplacement  = \"REPLACE(%s, ?, '')\"\n\t\t\treplaceField = searchField\n\t\t\treplaced     string\n\t\t\targsField    = []any{}\n\t\t)\n\n\t\tres = fields\n\t\tfor i, word := range words {\n\t\t\tif i == 0 {\n\t\t\t\targsField = append(argsField, word)\n\t\t\t\treplaced = fmt.Sprintf(replacement, replaceField)\n\t\t\t} else {\n\t\t\t\targsField = append(argsField, word)\n\t\t\t\treplaced = fmt.Sprintf(replacement, replaced)\n\t\t\t}\n\t\t}\n\t\targs = append(args, argsField...)\n\n\t\trelevance = fmt.Sprintf(relevance, replaced)\n\t\trelevanceRes = append(relevanceRes, relevance)\n\t}\n\n\tres = append(res, \"(\"+strings.Join(relevanceRes, \" + \")+\") as relevance\")\n\treturn\n}\n\nfunc filterWords(words []string) (res []string) {\n\tfor _, word := range words {\n\t\tif strings.TrimSpace(word) != \"\" {\n\t\t\tres = append(res, word)\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/search_sync/search_sync.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage search_sync\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc NewPluginSyncer(data *data.Data) plugin.SearchSyncer {\n\treturn &PluginSyncer{data: data}\n}\n\ntype PluginSyncer struct {\n\tdata *data.Data\n}\n\nfunc (p *PluginSyncer) GetAnswersPage(ctx context.Context, page, pageSize int) (\n\tanswerList []*plugin.SearchContent, err error) {\n\tanswers := make([]*entity.Answer, 0)\n\tstartNum := (page - 1) * pageSize\n\terr = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&answers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.convertAnswers(ctx, answers)\n}\n\nfunc (p *PluginSyncer) GetQuestionsPage(ctx context.Context, page, pageSize int) (\n\tquestionList []*plugin.SearchContent, err error) {\n\tquestions := make([]*entity.Question, 0)\n\tstartNum := (page - 1) * pageSize\n\terr = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&questions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.convertQuestions(ctx, questions)\n}\n\nfunc (p *PluginSyncer) convertAnswers(ctx context.Context, answers []*entity.Answer) (\n\tanswerList []*plugin.SearchContent, err error) {\n\tfor _, answer := range answers {\n\t\tquestion := &entity.Question{}\n\t\texist, err := p.data.DB.Context(ctx).Where(\"id = ?\", answer.QuestionID).Get(question)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get question failed %s\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif !exist {\n\t\t\tcontinue\n\t\t}\n\n\t\ttagListList := make([]*entity.TagRel, 0)\n\t\ttags := make([]string, 0)\n\t\terr = p.data.DB.Context(ctx).Where(\"object_id = ?\", uid.DeShortID(question.ID)).\n\t\t\tWhere(\"status = ?\", entity.TagRelStatusAvailable).Find(&tagListList)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get tag list failed %s\", err)\n\t\t}\n\t\tfor _, tag := range tagListList {\n\t\t\ttags = append(tags, tag.TagID)\n\t\t}\n\n\t\tcontent := &plugin.SearchContent{\n\t\t\tObjectID:    answer.ID,\n\t\t\tTitle:       question.Title,\n\t\t\tType:        constant.AnswerObjectType,\n\t\t\tContent:     answer.ParsedText,\n\t\t\tAnswers:     0,\n\t\t\tStatus:      plugin.SearchContentStatus(answer.Status),\n\t\t\tTags:        tags,\n\t\t\tQuestionID:  answer.QuestionID,\n\t\t\tUserID:      answer.UserID,\n\t\t\tViews:       int64(question.ViewCount),\n\t\t\tCreated:     answer.CreatedAt.Unix(),\n\t\t\tActive:      answer.UpdatedAt.Unix(),\n\t\t\tScore:       int64(answer.VoteCount),\n\t\t\tHasAccepted: answer.Accepted == schema.AnswerAcceptedEnable,\n\t\t}\n\t\tanswerList = append(answerList, content)\n\t}\n\treturn answerList, nil\n}\n\nfunc (p *PluginSyncer) convertQuestions(ctx context.Context, questions []*entity.Question) (\n\tquestionList []*plugin.SearchContent, err error) {\n\tfor _, question := range questions {\n\t\ttagListList := make([]*entity.TagRel, 0)\n\t\ttags := make([]string, 0)\n\t\terr := p.data.DB.Context(ctx).Where(\"object_id = ?\", question.ID).\n\t\t\tWhere(\"status = ?\", entity.TagRelStatusAvailable).Find(&tagListList)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get tag list failed %s\", err)\n\t\t}\n\t\tfor _, tag := range tagListList {\n\t\t\ttags = append(tags, tag.TagID)\n\t\t}\n\t\tcontent := &plugin.SearchContent{\n\t\t\tObjectID:    question.ID,\n\t\t\tTitle:       question.Title,\n\t\t\tType:        constant.QuestionObjectType,\n\t\t\tContent:     question.ParsedText,\n\t\t\tAnswers:     int64(question.AnswerCount),\n\t\t\tStatus:      plugin.SearchContentStatus(question.Status),\n\t\t\tTags:        tags,\n\t\t\tQuestionID:  question.ID,\n\t\t\tUserID:      question.UserID,\n\t\t\tViews:       int64(question.ViewCount),\n\t\t\tCreated:     question.CreatedAt.Unix(),\n\t\t\tActive:      question.UpdatedAt.Unix(),\n\t\t\tScore:       int64(question.VoteCount),\n\t\t\tHasAccepted: question.AcceptedAnswerID != \"\" && question.AcceptedAnswerID != \"0\",\n\t\t}\n\t\tquestionList = append(questionList, content)\n\t}\n\treturn questionList, nil\n}\n"
  },
  {
    "path": "internal/repo/site_info/siteinfo_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage site_info\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n)\n\ntype siteInfoRepo struct {\n\tdata *data.Data\n}\n\nfunc NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo {\n\treturn &siteInfoRepo{\n\t\tdata: data,\n\t}\n}\n\n// SaveByType save site setting by type\nfunc (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) {\n\told := &entity.SiteInfo{}\n\texist, err := sr.data.DB.Context(ctx).Where(builder.Eq{\"type\": siteType}).Get(old)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif exist {\n\t\t_, err = sr.data.DB.Context(ctx).ID(old.ID).Update(data)\n\t} else {\n\t\t_, err = sr.data.DB.Context(ctx).Insert(data)\n\t}\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tsr.setCache(ctx, siteType, data)\n\treturn\n}\n\n// GetByType get site info by type\nfunc (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (siteInfo *entity.SiteInfo, exist bool, err error) {\n\tif len(withoutCache) == 0 {\n\t\tsiteInfo = sr.getCache(ctx, siteType)\n\t\tif siteInfo != nil {\n\t\t\treturn siteInfo, true, nil\n\t\t}\n\t}\n\tsiteInfo = &entity.SiteInfo{}\n\texist, err = sr.data.DB.Context(ctx).Where(builder.Eq{\"type\": siteType}).Get(siteInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn nil, false, err\n\t}\n\tif exist {\n\t\tsr.setCache(ctx, siteType, siteInfo)\n\t}\n\treturn\n}\n\nfunc (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) {\n\tsiteInfoCache, exist, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tsiteInfo = &entity.SiteInfo{}\n\t_ = json.Unmarshal([]byte(siteInfoCache), siteInfo)\n\treturn siteInfo\n}\n\nfunc (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) {\n\tsiteInfoCache, _ := json.Marshal(siteInfo)\n\terr := sr.data.Cache.SetString(ctx,\n\t\tconstant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n}\n\nfunc (sr *siteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) {\n\tsiteInfo := &entity.SiteInfo{}\n\tcount, err := sr.data.DB.Context(ctx).\n\t\tTable(\"site_info\").\n\t\tWhere(builder.Eq{\"type\": \"branding\"}).\n\t\tAnd(builder.Like{\"content\", \"%\" + filePath + \"%\"}).\n\t\tCount(&siteInfo)\n\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\treturn count > 0, nil\n}\n"
  },
  {
    "path": "internal/repo/tag/tag_rel_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage tag\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/xorm\"\n)\n\n// tagRelRepo tag rel repository\ntype tagRelRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewTagRelRepo new repository\nfunc NewTagRelRepo(data *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo) tagcommon.TagRelRepo {\n\treturn &tagRelRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// AddTagRelList add tag list\nfunc (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) {\n\tfor _, item := range tagList {\n\t\titem.ObjectID = uid.DeShortID(item.ObjectID)\n\t}\n\t_, err = tr.data.DB.Context(ctx).Insert(tagList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range tagList {\n\t\t\titem.ObjectID = uid.EnShortID(item.ObjectID)\n\t\t}\n\t}\n\treturn\n}\n\n// RemoveTagRelListByObjectID delete tag list\nfunc (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\tobjectID = uid.DeShortID(objectID)\n\t_, err = tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RecoverTagRelListByObjectID recover tag list\nfunc (tr *tagRelRepo) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\tobjectID = uid.DeShortID(objectID)\n\t_, err = tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\tobjectID = uid.DeShortID(objectID)\n\t_, err = tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).And(\"status = ?\", entity.TagRelStatusAvailable).Cols(\"status\").Update(&entity.TagRel{Status: entity.TagRelStatusHide})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\tobjectID = uid.DeShortID(objectID)\n\t_, err = tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).And(\"status = ?\", entity.TagRelStatusHide).Cols(\"status\").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RemoveTagRelListByIDs delete tag list\nfunc (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) {\n\t_, err = tr.data.DB.Context(ctx).In(\"id\", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetObjectTagRelWithoutStatus get object tag relation no matter status\nfunc (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) (\n\ttagRel *entity.TagRel, exist bool, err error,\n) {\n\tobjectID = uid.DeShortID(objectID)\n\ttagRel = &entity.TagRel{}\n\tsession := tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID).And(\"tag_id = ?\", tagID)\n\texist, err = session.Get(tagRel)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\ttagRel.ObjectID = uid.EnShortID(tagRel.ObjectID)\n\t}\n\treturn\n}\n\n// EnableTagRelByIDs update tag status to available\nfunc (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) {\n\tstatus := entity.TagRelStatusAvailable\n\tif hide {\n\t\tstatus = entity.TagRelStatusHide\n\t}\n\t_, err = tr.data.DB.Context(ctx).In(\"id\", ids).Update(&entity.TagRel{Status: status})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetObjectTagRelList get object tag relation list all\nfunc (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) {\n\tobjectID = uid.DeShortID(objectID)\n\ttagListList = make([]*entity.TagRel, 0)\n\tsession := tr.data.DB.Context(ctx).Where(\"object_id = ?\", objectID)\n\tsession.In(\"status\", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide})\n\terr = session.Find(&tagListList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range tagListList {\n\t\t\titem.ObjectID = uid.EnShortID(item.ObjectID)\n\t\t}\n\t}\n\treturn\n}\n\n// BatchGetObjectTagRelList get object tag relation list all\nfunc (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) {\n\tfor num, item := range objectIds {\n\t\tobjectIds[num] = uid.DeShortID(item)\n\t}\n\ttagListList = make([]*entity.TagRel, 0)\n\tsession := tr.data.DB.Context(ctx).In(\"object_id\", objectIds)\n\tsession.Where(\"status = ?\", entity.TagRelStatusAvailable)\n\terr = session.Find(&tagListList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range tagListList {\n\t\t\titem.ObjectID = uid.EnShortID(item.ObjectID)\n\t\t}\n\t}\n\treturn\n}\n\n// CountTagRelByTagID count tag relation\nfunc (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) {\n\tcount, err = tr.data.DB.Context(ctx).Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagRelDefaultStatusByObjectID get tag rel default status\nfunc (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) {\n\tquestion := entity.Question{}\n\texist, err := tr.data.DB.Context(ctx).ID(objectID).Cols(\"show\", \"status\").Get(&question)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif exist && (question.Show == entity.QuestionHide || question.Status == entity.QuestionStatusDeleted) {\n\t\treturn entity.TagRelStatusHide, nil\n\t}\n\treturn entity.TagRelStatusAvailable, nil\n}\n\n// MigrateTagObjects migrate tag objects\nfunc (tr *tagRelRepo) MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error {\n\t_, err := tr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {\n\t\t// 1. Get all objects related to source tag\n\t\tvar sourceObjects []entity.TagRel\n\t\terr = session.Where(\"tag_id = ?\", sourceTagId).Find(&sourceObjects)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\t// 2. Get existing target tag relations\n\t\tvar existingTargets []entity.TagRel\n\t\terr = session.Where(\"tag_id = ?\", targetTagId).Find(&existingTargets)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\t// Create map of existing target objects for quick lookup\n\t\texistingMap := make(map[string]bool)\n\t\tfor _, target := range existingTargets {\n\t\t\texistingMap[target.ObjectID] = true\n\t\t}\n\n\t\t// 3. Create new relations for objects not already tagged with target\n\t\tnewRelations := make([]*entity.TagRel, 0)\n\t\tfor _, source := range sourceObjects {\n\t\t\tif !existingMap[source.ObjectID] {\n\t\t\t\tnewRelations = append(newRelations, &entity.TagRel{\n\t\t\t\t\tTagID:    targetTagId,\n\t\t\t\t\tObjectID: source.ObjectID,\n\t\t\t\t\tStatus:   source.Status,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(newRelations) > 0 {\n\t\t\t_, err = session.Insert(newRelations)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t\t}\n\t\t}\n\n\t\t// 4. Remove old relations\n\t\t_, err = session.Where(\"tag_id = ?\", sourceTagId).Delete(&entity.TagRel{})\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\n\t\treturn nil, nil\n\t})\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/repo/tag/tag_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage tag\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n)\n\n// tagRepo tag repository\ntype tagRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewTagRepo new repository\nfunc NewTagRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n) tag_common.TagRepo {\n\treturn &tagRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// RemoveTag delete tag\nfunc (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) {\n\tsession := tr.data.DB.Context(ctx).Where(builder.Eq{\"id\": tagID})\n\t_, err = session.Update(&entity.Tag{Status: entity.TagStatusDeleted})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateTag update tag\nfunc (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) {\n\t_, err = tr.data.DB.Context(ctx).Where(builder.Eq{\"id\": tag.ID}).Update(tag)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// RecoverTag recover deleted tag\nfunc (tr *tagRepo) RecoverTag(ctx context.Context, tagID string) (err error) {\n\t_, err = tr.data.DB.Context(ctx).ID(tagID).Update(&entity.Tag{Status: entity.TagStatusAvailable})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// MustGetTagByNameOrID get tag by name or id\nfunc (tr *tagRepo) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (\n\ttag *entity.Tag, exist bool, err error) {\n\tif len(tagID) == 0 && len(slugName) == 0 {\n\t\treturn nil, false, nil\n\t}\n\ttag = &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx)\n\tif len(tagID) > 0 {\n\t\tsession.ID(tagID)\n\t}\n\tif len(slugName) > 0 {\n\t\tsession.Where(builder.Eq{\"slug_name\": slugName})\n\t}\n\texist, err = session.Get(tag)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateTagSynonym update synonym tag\nfunc (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64,\n\tmainTagSlugName string,\n) (err error) {\n\tbean := &entity.Tag{MainTagID: mainTagID, MainTagSlugName: mainTagSlugName}\n\tsession := tr.data.DB.Context(ctx).In(\"slug_name\", tagSlugNameList).MustCols(\"main_tag_id\", \"main_tag_slug_name\")\n\t_, err = session.Update(bean)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagRepo) GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) {\n\tcount, err = tr.data.DB.Context(ctx).Count(&entity.Tag{MainTagID: converter.StringToInt64(tagID), Status: entity.TagStatusAvailable})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagRepo) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) {\n\tsession := tr.data.DB.Context(ctx).Table(entity.Tag{}.TableName()).Where(builder.Eq{\"status\": entity.TagStatusAvailable, \"main_tag_id\": converter.StringToInt64(mainTagID)}).Cols(\"id\")\n\terr = session.Find(&tagIDs)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagList get tag list all\nfunc (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {\n\ttagList = make([]*entity.Tag, 0)\n\tsession := tr.data.DB.Context(ctx).Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\terr = session.Find(&tagList, tag)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/tag_common/tag_common_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage tag_common\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"xorm.io/builder\"\n)\n\n// tagCommonRepo tag repository\ntype tagCommonRepo struct {\n\tdata         *data.Data\n\tuniqueIDRepo unique.UniqueIDRepo\n}\n\n// NewTagCommonRepo new repository\nfunc NewTagCommonRepo(\n\tdata *data.Data,\n\tuniqueIDRepo unique.UniqueIDRepo,\n) tagcommon.TagCommonRepo {\n\treturn &tagCommonRepo{\n\t\tdata:         data,\n\t\tuniqueIDRepo: uniqueIDRepo,\n\t}\n}\n\n// GetTagListByIDs get tag list all\nfunc (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {\n\ttagList = make([]*entity.Tag, 0)\n\tsession := tr.data.DB.Context(ctx).In(\"id\", ids)\n\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\terr = session.OrderBy(\"recommend desc,reserved desc,id desc\").Find(&tagList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagBySlugName get tag by slug name\nfunc (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {\n\ttagInfo = &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx).Where(\"LOWER(slug_name) = ?\", slugName)\n\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\texist, err = session.Get(tagInfo)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagListByName get tag list all like name\nfunc (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) {\n\tcond := &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx)\n\tif len(name) > 0 {\n\t\tsession.Where(\"slug_name LIKE ? OR display_name LIKE ?\", strings.ToLower(name)+\"%\", name+\"%\")\n\t}\n\tvar columns []string\n\tif recommend {\n\t\tcolumns = append(columns, \"recommend\")\n\t\tcond.Recommend = true\n\t}\n\tif reserved {\n\t\tcolumns = append(columns, \"reserved\")\n\t\tcond.Reserved = true\n\t}\n\tif len(columns) > 0 {\n\t\tsession.UseBool(columns...)\n\t}\n\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\n\ttagList = make([]*entity.Tag, 0)\n\terr = session.OrderBy(\"recommend DESC,reserved DESC,slug_name ASC\").Find(&tagList, cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) {\n\ttagList = make([]*entity.Tag, 0)\n\tcond := &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx).Where(\"\")\n\tcond.Recommend = true\n\t// session.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\tsession.Asc(\"slug_name\")\n\tsession.UseBool(\"recommend\")\n\terr = session.Find(&tagList, cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) {\n\ttagList = make([]*entity.Tag, 0)\n\tcond := &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx).Where(\"\")\n\tcond.Reserved = true\n\t// session.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\tsession.Asc(\"slug_name\")\n\tsession.UseBool(\"reserved\")\n\terr = session.Find(&tagList, cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagListByNames get tag list all like name\nfunc (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {\n\ttagList = make([]*entity.Tag, 0)\n\tsession := tr.data.DB.Context(ctx).In(\"slug_name\", names).UseBool(\"recommend\", \"reserved\")\n\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\terr = session.OrderBy(\"recommend desc,reserved desc,id desc\").Find(&tagList)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagByID get tag one\nfunc (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (\n\ttag *entity.Tag, exist bool, err error,\n) {\n\ttag = &entity.Tag{}\n\tsession := tr.data.DB.Context(ctx).Where(builder.Eq{\"id\": tagID})\n\tif !includeDeleted {\n\t\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\t}\n\texist, err = session.Get(tag)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetTagPage get tag page\nfunc (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (\n\ttagList []*entity.Tag, total int64, err error,\n) {\n\ttagList = make([]*entity.Tag, 0)\n\tsession := tr.data.DB.Context(ctx)\n\n\tif len(tag.SlugName) > 0 {\n\t\tmainTagCond := builder.And(\n\t\t\tbuilder.Or(\n\t\t\t\tbuilder.Like{\"slug_name\", fmt.Sprintf(\"LOWER(%s)\", tag.SlugName)},\n\t\t\t\tbuilder.Like{\"display_name\", tag.SlugName},\n\t\t\t),\n\t\t\tbuilder.Eq{\"main_tag_id\": 0},\n\t\t)\n\t\tsynonymCond := builder.And(\n\t\t\tbuilder.Eq{\"slug_name\": tag.SlugName},\n\t\t\tbuilder.Neq{\"main_tag_id\": 0},\n\t\t)\n\t\tsession.Where(builder.Or(mainTagCond, synonymCond))\n\t\ttag.SlugName = \"\"\n\t} else {\n\t\tsession.Where(builder.Eq{\"main_tag_id\": 0})\n\t}\n\tsession.Where(builder.Eq{\"status\": entity.TagStatusAvailable})\n\n\tswitch queryCond {\n\tcase \"popular\":\n\t\tsession.Desc(\"question_count\")\n\tcase \"name\":\n\t\tsession.Asc(\"slug_name\")\n\tcase \"newest\":\n\t\tsession.Desc(\"created_at\")\n\t}\n\n\ttotal, err = pager.Help(page, pageSize, &tagList, tag, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\n\tfor i := 0; i < len(tagList); i++ {\n\t\tif tagList[i].MainTagID != 0 {\n\t\t\tmainTag, exist, errSynonym := tr.GetTagByID(ctx, strconv.FormatInt(tagList[i].MainTagID, 10), false)\n\t\t\tif errSynonym != nil {\n\t\t\t\terr = errors.InternalServer(reason.DatabaseError).WithError(errSynonym).WithStack()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\ttagList[i] = mainTag\n\t\t\t}\n\t\t}\n\t}\n\n\treturn\n}\n\n// AddTagList add tag\nfunc (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {\n\taddTags := make([]*entity.Tag, 0)\n\tfor _, item := range tagList {\n\t\texist, err := tr.updateDeletedTag(ctx, item)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tcontinue\n\t\t}\n\t\taddTags = append(addTags, item)\n\t\titem.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\titem.RevisionID = \"0\"\n\t}\n\tif len(addTags) == 0 {\n\t\treturn nil\n\t}\n\t_, err = tr.data.DB.Context(ctx).Insert(addTags)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagCommonRepo) updateDeletedTag(ctx context.Context, tag *entity.Tag) (exist bool, err error) {\n\told := &entity.Tag{SlugName: tag.SlugName}\n\texist, err = tr.data.DB.Context(ctx).Get(old)\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist || old.Status != entity.TagStatusDeleted {\n\t\treturn false, nil\n\t}\n\ttag.ID = old.ID\n\ttag.Status = entity.TagStatusAvailable\n\ttag.RevisionID = \"0\"\n\tif _, err = tr.data.DB.Context(ctx).ID(tag.ID).Update(tag); err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn true, nil\n}\n\n// UpdateTagQuestionCount update tag question count\nfunc (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {\n\tcond := &entity.Tag{QuestionCount: questionCount}\n\t_, err = tr.data.DB.Context(ctx).Where(builder.Eq{\"id\": tagID}).MustCols(\"question_count\").Update(cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) {\n\tbean := &entity.Tag{}\n\tswitch attribute {\n\tcase \"recommend\":\n\t\tbean.Recommend = value\n\tcase \"reserved\":\n\t\tbean.Reserved = value\n\tdefault:\n\t\treturn\n\t}\n\tsession := tr.data.DB.Context(ctx).In(\"slug_name\", tags).Cols(attribute).UseBool(attribute)\n\t_, err = session.Update(bean)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/repo/unique/uniqid_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage unique\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/unique\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// uniqueIDRepo Unique id repository\ntype uniqueIDRepo struct {\n\tdata *data.Data\n}\n\n// NewUniqueIDRepo new repository\nfunc NewUniqueIDRepo(data *data.Data) unique.UniqueIDRepo {\n\treturn &uniqueIDRepo{\n\t\tdata: data,\n\t}\n}\n\n// GenUniqueIDStr generate unique id string\n// 1 + 00x(objectType) + 000000000000x(id)\nfunc (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) {\n\tobjectType := constant.ObjectTypeStrMapping[key]\n\tbean := &entity.Uniqid{UniqidType: objectType}\n\t_, err = ur.data.DB.Context(ctx).Insert(bean)\n\tif err != nil {\n\t\treturn \"\", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn fmt.Sprintf(\"1%03d%013d\", objectType, bean.ID), nil\n}\n"
  },
  {
    "path": "internal/repo/user/user_backyard_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"xorm.io/builder\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/user_admin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// userAdminRepo user repository\ntype userAdminRepo struct {\n\tdata     *data.Data\n\tauthRepo auth.AuthRepo\n}\n\n// NewUserAdminRepo new repository\nfunc NewUserAdminRepo(data *data.Data, authRepo auth.AuthRepo) user_admin.UserAdminRepo {\n\treturn &userAdminRepo{\n\t\tdata:     data,\n\t\tauthRepo: authRepo,\n\t}\n}\n\n// UpdateUserStatus update user status\nfunc (ur *userAdminRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int,\n\temail string, suspendedUntil time.Time,\n) (err error) {\n\tcond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email}\n\tswitch userStatus {\n\tcase entity.UserStatusSuspended:\n\t\tcond.SuspendedAt = time.Now()\n\t\tcond.SuspendedUntil = suspendedUntil\n\tcase entity.UserStatusDeleted:\n\t\tcond.DeletedAt = time.Now()\n\tcase entity.UserStatusAvailable:\n\t\t// When restoring user status, clear suspended until time to zero\n\t\tcond.SuspendedUntil = time.Time{}\n\t}\n\t_, err = ur.data.DB.Context(ctx).ID(userID).MustCols(\"status\", \"mail_status\", \"e_mail\", \"suspended_at\", \"suspended_until\", \"deleted_at\").Update(cond)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\tuserCacheInfo := &entity.UserCacheInfo{\n\t\tUserID:      userID,\n\t\tEmailStatus: mailStatus,\n\t\tUserStatus:  userStatus,\n\t}\n\tt, _ := json.Marshal(userCacheInfo)\n\tlog.Infof(\"user change status: %s\", string(t))\n\terr = ur.authRepo.SetUserStatus(ctx, userID, userCacheInfo)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// AddUser add user\nfunc (ur *userAdminRepo) AddUser(ctx context.Context, user *entity.User) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Insert(user)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// AddUsers add users\nfunc (ur *userAdminRepo) AddUsers(ctx context.Context, users []*entity.User) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Insert(users)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateUserPassword update user password\nfunc (ur *userAdminRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {\n\t_, err = ur.data.DB.Context(ctx).ID(userID).Update(&entity.User{Pass: password})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUserInfo get user info\nfunc (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {\n\tuser = &entity.User{}\n\texist, err = ur.data.DB.Context(ctx).ID(userID).Get(user)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn\n\t}\n\terr = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\treturn\n}\n\n// GetUserInfoByEmail get user info\nfunc (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {\n\tuserInfo := &entity.User{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"e_mail = ?\", email).\n\t\tWhere(\"status != ?\", entity.UserStatusDeleted).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn\n\t}\n\terr = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\treturn\n}\n\n// GetUserPage get user page\nfunc (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,\n\tusernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) {\n\tusers = make([]*entity.User, 0)\n\tsession := ur.data.DB.Context(ctx)\n\tswitch user.Status {\n\tcase entity.UserStatusDeleted:\n\t\tsession.Desc(\"`user`.deleted_at\")\n\tcase entity.UserStatusSuspended:\n\t\tsession.Desc(\"`user`.suspended_at\")\n\tdefault:\n\t\tsession.Desc(\"`user`.created_at\")\n\t}\n\n\tif len(usernameOrDisplayName) > 0 {\n\t\tsession.And(builder.Or(\n\t\t\tbuilder.Like{\"`user`.username\", usernameOrDisplayName},\n\t\t\tbuilder.Like{\"`user`.display_name\", usernameOrDisplayName},\n\t\t))\n\t}\n\tif isStaff {\n\t\tsession.Join(\"INNER\", \"user_role_rel\", \"`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1\")\n\t}\n\n\ttotal, err = pager.Help(page, pageSize, &users, user, session)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\ttryToDecorateUserListFromUserCenter(ctx, ur.data, users)\n\treturn\n}\n\n// DeletePermanentlyUsers delete permanently users\nfunc (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Where(\"status = ?\", entity.UserStatusDeleted).Delete(&entity.User{})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetExpiredSuspendedUsers gets all suspended users whose suspension has expired\nfunc (ur *userAdminRepo) GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) {\n\tusers = make([]*entity.User, 0)\n\tnow := time.Now()\n\n\terr = ur.data.DB.Context(ctx).\n\t\tWhere(\"status = ?\", entity.UserStatusSuspended).\n\t\tWhere(\"suspended_until < ?\", now).\n\t\tFind(&users)\n\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\treturn users, nil\n}\n"
  },
  {
    "path": "internal/repo/user/user_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// userRepo user repository\ntype userRepo struct {\n\tdata *data.Data\n}\n\n// NewUserRepo new repository\nfunc NewUserRepo(data *data.Data) usercommon.UserRepo {\n\treturn &userRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddUser add user\nfunc (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {\n\t_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {\n\t\tsession = session.Context(ctx)\n\t\tuserInfo := &entity.User{}\n\t\texist, err := session.Where(\"username = ?\", user.Username).Get(userInfo)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\tif exist {\n\t\t\treturn nil, errors.InternalServer(reason.UsernameDuplicate)\n\t\t}\n\t\t_, err = session.Insert(user)\n\t\tif err != nil {\n\t\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\t}\n\t\treturn nil, nil\n\t})\n\treturn\n}\n\n// IncreaseAnswerCount increase answer count\nfunc (ur *userRepo) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) {\n\tuser := &entity.User{}\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Incr(\"answer_count\", amount).Update(user)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// IncreaseQuestionCount increase question count\nfunc (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) {\n\tuser := &entity.User{}\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Incr(\"question_count\", amount).Update(user)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {\n\tuser := &entity.User{}\n\tuser.QuestionCount = int(count)\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"question_count\").Update(user)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {\n\tuser := &entity.User{}\n\tuser.AnswerCount = count\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"answer_count\").Update(user)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// UpdateLastLoginDate update last login date\nfunc (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) {\n\tuser := &entity.User{LastLoginDate: time.Now()}\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"last_login_date\").Update(user)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// UpdateEmailStatus update email status\nfunc (ur *userRepo) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error {\n\tcond := &entity.User{MailStatus: emailStatus}\n\t_, err := ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"mail_status\").Update(cond)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// UpdateNoticeStatus update notice status\nfunc (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error {\n\tcond := &entity.User{NoticeStatus: noticeStatus}\n\t_, err := ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"notice_status\").Update(cond)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error {\n\t_, err := ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Cols(\"pass\").Update(&entity.User{Pass: pass})\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Update(&entity.User{EMail: email})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ur *userRepo) UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) {\n\tsession := ur.data.DB.Context(ctx).Where(\"id = ?\", userID)\n\t_, err = session.Cols(\"language\", \"color_scheme\").Update(&entity.User{Language: language, ColorScheme: colorSchema})\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateInfo update user info\nfunc (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userInfo.ID).\n\t\tCols(\"username\", \"display_name\", \"avatar\", \"bio\", \"bio_html\", \"website\", \"location\").Update(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateUserProfile update user profile\nfunc (ur *userRepo) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userInfo.ID).\n\t\tCols(\"username\", \"e_mail\", \"mail_status\", \"display_name\").Update(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetByUserID get user info by user id\nfunc (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) {\n\tuserInfo = &entity.User{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"id = ?\", userID).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\terr = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\treturn\n}\n\nfunc (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) {\n\tlist := make([]*entity.User, 0)\n\terr := ur.data.DB.Context(ctx).In(\"id\", ids).Find(&list)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\ttryToDecorateUserListFromUserCenter(ctx, ur.data, list)\n\treturn list, nil\n}\n\n// GetByUsername get user by username\nfunc (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) {\n\tuserInfo = &entity.User{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"username = ?\", username).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn\n\t}\n\terr = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\treturn\n}\n\nfunc (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {\n\tlist := make([]*entity.User, 0)\n\terr := ur.data.DB.Context(ctx).Where(\"status =?\", entity.UserStatusAvailable).In(\"username\", usernames).Find(&list)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t\treturn list, err\n\t}\n\ttryToDecorateUserListFromUserCenter(ctx, ur.data, list)\n\treturn list, nil\n}\n\n// GetByEmail get user by email\nfunc (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {\n\tuserInfo = &entity.User{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"e_mail = ?\", email).\n\t\tWhere(\"status != ?\", entity.UserStatusDeleted).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\nfunc (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {\n\tsession := ur.data.DB.Context(ctx)\n\tsession.Where(\"status = ? OR status = ?\", entity.UserStatusAvailable, entity.UserStatusSuspended)\n\tcount, err = session.Count(&entity.User{})\n\tif err != nil {\n\t\treturn count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn count, nil\n}\n\nfunc (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int,\n\tonlyStaff bool) (userList []*entity.User, err error) {\n\tuserList = make([]*entity.User, 0)\n\tsession := ur.data.DB.Context(ctx)\n\tif onlyStaff {\n\t\tsession.Join(\"INNER\", \"user_role_rel\", \"`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1\")\n\t}\n\tsession.Where(\"status = ?\", entity.UserStatusAvailable)\n\tsession.Where(\"username LIKE ? OR display_name LIKE ?\", strings.ToLower(name)+\"%\", name+\"%\")\n\tsession.OrderBy(\"username ASC, `user`.id DESC\")\n\tsession.Limit(limit)\n\terr = session.Find(&userList)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\ttryToDecorateUserListFromUserCenter(ctx, ur.data, userList)\n\treturn\n}\n\nfunc tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {\n\tif original == nil {\n\t\treturn nil\n\t}\n\tuc, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tuserInfo := &entity.UserExternalLogin{}\n\tsession := data.DB.Context(ctx).Where(\"user_id = ?\", original.ID)\n\tsession.Where(\"provider = ?\", uc.Info().SlugName)\n\texist, err := session.Get(userInfo)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\n\tuserCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn errors.BadRequest(reason.UserNotFound).WithError(err).WithStack()\n\t}\n\n\tdecorateByUserCenterUser(original, userCenterBasicUserInfo)\n\treturn nil\n}\n\nfunc tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) {\n\tuc, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn\n\t}\n\n\tids := make([]string, 0)\n\toriginalUserIDMapping := make(map[string]*entity.User, 0)\n\tfor _, user := range original {\n\t\toriginalUserIDMapping[user.ID] = user\n\t\tids = append(ids, user.ID)\n\t}\n\n\tuserExternalLoginList := make([]*entity.UserExternalLogin, 0)\n\tsession := data.DB.Context(ctx).Where(\"provider = ?\", uc.Info().SlugName)\n\tsession.In(\"user_id\", ids)\n\terr := session.Find(&userExternalLoginList)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tuserExternalIDs := make([]string, 0)\n\toriginalExternalIDMapping := make(map[string]*entity.User, 0)\n\tfor _, u := range userExternalLoginList {\n\t\toriginalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID]\n\t\tuserExternalIDs = append(userExternalIDs, u.ExternalID)\n\t}\n\tif len(userExternalIDs) == 0 {\n\t\treturn\n\t}\n\n\tucUsers, err := uc.UserList(userExternalIDs)\n\tif err != nil {\n\t\tlog.Errorf(\"get user list from user center failed: %v, %v\", err, userExternalIDs)\n\t\treturn\n\t}\n\n\tfor _, ucUser := range ucUsers {\n\t\tdecorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser)\n\t}\n}\n\nfunc decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) {\n\tif original == nil || ucUser == nil {\n\t\treturn\n\t}\n\t// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.\n\tif original.Username != ucUser.Username {\n\t\tlog.Warnf(\"user %s username is inconsistent with user center\", original.ID)\n\t}\n\tif len(ucUser.DisplayName) > 0 {\n\t\toriginal.DisplayName = ucUser.DisplayName\n\t}\n\tif len(ucUser.Email) > 0 {\n\t\toriginal.EMail = ucUser.Email\n\t}\n\tif len(ucUser.Avatar) > 0 {\n\t\toriginal.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()\n\t}\n\tif len(ucUser.Mobile) > 0 {\n\t\toriginal.Mobile = ucUser.Mobile\n\t}\n\tif len(ucUser.Bio) > 0 {\n\t\toriginal.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML\n\t}\n\n\t// If plugin enable rank agent, use rank from user center.\n\tif plugin.RankAgentEnabled() {\n\t\toriginal.Rank = ucUser.Rank\n\t}\n\tif ucUser.Status != plugin.UserStatusAvailable {\n\t\toriginal.Status = int(ucUser.Status)\n\t}\n}\n\nfunc (ur *userRepo) IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) {\n\tuser := &entity.User{}\n\tcount, err := ur.data.DB.Context(ctx).\n\t\tTable(\"user\").\n\t\tWhere(builder.Like{\"avatar\", \"%\" + filePath + \"%\"}).\n\t\tCount(&user)\n\n\tif err != nil {\n\t\treturn false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\n\treturn count > 0, nil\n}\n"
  },
  {
    "path": "internal/repo/user_external_login/user_external_login_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_external_login\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\ntype userExternalLoginRepo struct {\n\tdata *data.Data\n}\n\n// NewUserExternalLoginRepo new repository\nfunc NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo {\n\treturn &userExternalLoginRepo{\n\t\tdata: data,\n\t}\n}\n\n// AddUserExternalLogin add external login information\nfunc (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) {\n\t_, err = ur.data.DB.Context(ctx).Insert(user)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// UpdateInfo update user info\nfunc (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) {\n\t_, err = ur.data.DB.Context(ctx).ID(userInfo.ID).Update(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetByExternalID get by external ID\nfunc (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) (\n\tuserInfo *entity.UserExternalLogin, exist bool, err error) {\n\tuserInfo = &entity.UserExternalLogin{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"external_id = ?\", externalID).Where(\"provider = ?\", provider).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetByUserID get by user ID\nfunc (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, userID string) (\n\tuserInfo *entity.UserExternalLogin, exist bool, err error) {\n\tuserInfo = &entity.UserExternalLogin{}\n\texist, err = ur.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Where(\"provider = ?\", provider).Get(userInfo)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// GetUserExternalLoginList get by external ID\nfunc (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (\n\tresp []*entity.UserExternalLogin, err error) {\n\tresp = make([]*entity.UserExternalLogin, 0)\n\terr = ur.data.DB.Context(ctx).Where(\"user_id = ?\", userID).OrderBy(\"updated_at DESC\").Find(&resp)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// DeleteUserExternalLogin delete external user login info\nfunc (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) {\n\tcond := &entity.UserExternalLogin{}\n\t_, err = ur.data.DB.Context(ctx).Where(\"user_id = ? AND external_id = ?\", userID, externalID).Delete(cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// DeleteUserExternalLoginByUserID delete external user login info by user ID\nfunc (ur *userExternalLoginRepo) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) {\n\tcond := &entity.UserExternalLogin{}\n\t_, err = ur.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Delete(cond)\n\tif err != nil {\n\t\terr = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn\n}\n\n// SetCacheUserExternalLoginInfo cache user info for external login\nfunc (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(\n\tctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {\n\tcacheData, _ := json.Marshal(info)\n\treturn ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key,\n\t\tstring(cacheData), constant.ConnectorUserExternalInfoCacheTime)\n}\n\n// GetCacheUserExternalLoginInfo cache user info for external login\nfunc (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(\n\tctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {\n\tres, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)\n\tif err != nil {\n\t\treturn info, err\n\t}\n\tif !exist {\n\t\treturn nil, nil\n\t}\n\tinfo = &schema.ExternalLoginUserInfoCache{}\n\t_ = json.Unmarshal([]byte(res), &info)\n\treturn info, nil\n}\n"
  },
  {
    "path": "internal/repo/user_notification_config/user_notification_config_repo.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_notification_config\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// userNotificationConfigRepo notification repository\ntype userNotificationConfigRepo struct {\n\tdata *data.Data\n}\n\n// NewUserNotificationConfigRepo new repository\nfunc NewUserNotificationConfigRepo(data *data.Data) user_notification_config.UserNotificationConfigRepo {\n\treturn &userNotificationConfigRepo{\n\t\tdata: data,\n\t}\n}\n\n// Add add notification config\nfunc (ur *userNotificationConfigRepo) Add(ctx context.Context, userIDs []string, source, channels string) (err error) {\n\tvar configs []*entity.UserNotificationConfig\n\tfor _, userID := range userIDs {\n\t\tconfigs = append(configs, &entity.UserNotificationConfig{\n\t\t\tUserID:   userID,\n\t\t\tSource:   source,\n\t\t\tChannels: channels,\n\t\t\tEnabled:  true,\n\t\t})\n\t}\n\t_, err = ur.data.DB.Context(ctx).Insert(configs)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// Save save notification config, if existed, update, if not exist, insert\nfunc (ur *userNotificationConfigRepo) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) {\n\told := &entity.UserNotificationConfig{UserID: uc.UserID, Source: uc.Source}\n\texist, err := ur.data.DB.Context(ctx).Get(old)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\tif exist {\n\t\told.Channels = uc.Channels\n\t\told.Enabled = uc.Enabled\n\t\t_, err = ur.data.DB.Context(ctx).ID(old.ID).UseBool(\"enabled\").Cols(\"channels\", \"enabled\").Update(old)\n\t} else {\n\t\t_, err = ur.data.DB.Context(ctx).Insert(uc)\n\t}\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// GetByUserID get notification config by user id\nfunc (ur *userNotificationConfigRepo) GetByUserID(ctx context.Context, userID string) (\n\t[]*entity.UserNotificationConfig, error) {\n\tvar configs []*entity.UserNotificationConfig\n\terr := ur.data.DB.Context(ctx).Where(\"user_id = ?\", userID).Find(&configs)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn configs, nil\n}\n\n// GetBySource get notification config by source\nfunc (ur *userNotificationConfigRepo) GetBySource(ctx context.Context, source constant.NotificationSource) (\n\t[]*entity.UserNotificationConfig, error) {\n\tvar configs []*entity.UserNotificationConfig\n\terr := ur.data.DB.Context(ctx).UseBool(\"enabled\").\n\t\tFind(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn configs, nil\n}\n\n// GetByUserIDAndSource get notification config by user id and source\nfunc (ur *userNotificationConfigRepo) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) (\n\tconf *entity.UserNotificationConfig, exist bool, err error) {\n\tconfig := &entity.UserNotificationConfig{UserID: userID, Source: string(source)}\n\texist, err = ur.data.DB.Context(ctx).Get(config)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn config, exist, nil\n}\n\n// GetByUsersAndSource get notification config by user ids and source\nfunc (ur *userNotificationConfigRepo) GetByUsersAndSource(\n\tctx context.Context, userIDs []string, source constant.NotificationSource) (\n\t[]*entity.UserNotificationConfig, error) {\n\tvar configs []*entity.UserNotificationConfig\n\terr := ur.data.DB.Context(ctx).UseBool(\"enabled\").In(\"user_id\", userIDs).\n\t\tFind(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()\n\t}\n\treturn configs, nil\n}\n"
  },
  {
    "path": "internal/router/answer_api_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/controller\"\n\t\"github.com/apache/answer/internal/controller_admin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype AnswerAPIRouter struct {\n\tlangController                *controller.LangController\n\tuserController                *controller.UserController\n\tcommentController             *controller.CommentController\n\treportController              *controller.ReportController\n\tvoteController                *controller.VoteController\n\ttagController                 *controller.TagController\n\tfollowController              *controller.FollowController\n\tcollectionController          *controller.CollectionController\n\tquestionController            *controller.QuestionController\n\tanswerController              *controller.AnswerController\n\tsearchController              *controller.SearchController\n\trevisionController            *controller.RevisionController\n\trankController                *controller.RankController\n\tadminUserController           *controller_admin.UserAdminController\n\treasonController              *controller.ReasonController\n\tthemeController               *controller_admin.ThemeController\n\tadminSiteInfoController       *controller_admin.SiteInfoController\n\tsiteInfoController            *controller.SiteInfoController\n\tnotificationController        *controller.NotificationController\n\tdashboardController           *controller.DashboardController\n\tuploadController              *controller.UploadController\n\tactivityController            *controller.ActivityController\n\troleController                *controller_admin.RoleController\n\tpluginController              *controller_admin.PluginController\n\tpermissionController          *controller.PermissionController\n\tuserPluginController          *controller.UserPluginController\n\treviewController              *controller.ReviewController\n\tmetaController                *controller.MetaController\n\tbadgeController               *controller.BadgeController\n\tadminBadgeController          *controller_admin.BadgeController\n\tapiKeyController              *controller_admin.AdminAPIKeyController\n\taiController                  *controller.AIController\n\taiConversationController      *controller.AIConversationController\n\taiConversationAdminController *controller_admin.AIConversationAdminController\n\tmcpController                 *controller.MCPController\n}\n\nfunc NewAnswerAPIRouter(\n\tlangController *controller.LangController,\n\tuserController *controller.UserController,\n\tcommentController *controller.CommentController,\n\treportController *controller.ReportController,\n\tvoteController *controller.VoteController,\n\ttagController *controller.TagController,\n\tfollowController *controller.FollowController,\n\tcollectionController *controller.CollectionController,\n\tquestionController *controller.QuestionController,\n\tanswerController *controller.AnswerController,\n\tsearchController *controller.SearchController,\n\trevisionController *controller.RevisionController,\n\trankController *controller.RankController,\n\tadminUserController *controller_admin.UserAdminController,\n\treasonController *controller.ReasonController,\n\tthemeController *controller_admin.ThemeController,\n\tadminSiteInfoController *controller_admin.SiteInfoController,\n\tsiteInfoController *controller.SiteInfoController,\n\tnotificationController *controller.NotificationController,\n\tdashboardController *controller.DashboardController,\n\tuploadController *controller.UploadController,\n\tactivityController *controller.ActivityController,\n\troleController *controller_admin.RoleController,\n\tpluginController *controller_admin.PluginController,\n\tpermissionController *controller.PermissionController,\n\tuserPluginController *controller.UserPluginController,\n\treviewController *controller.ReviewController,\n\tmetaController *controller.MetaController,\n\tbadgeController *controller.BadgeController,\n\tadminBadgeController *controller_admin.BadgeController,\n\tapiKeyController *controller_admin.AdminAPIKeyController,\n\taiController *controller.AIController,\n\taiConversationController *controller.AIConversationController,\n\taiConversationAdminController *controller_admin.AIConversationAdminController,\n\tmcpController *controller.MCPController,\n) *AnswerAPIRouter {\n\treturn &AnswerAPIRouter{\n\t\tlangController:                langController,\n\t\tuserController:                userController,\n\t\tcommentController:             commentController,\n\t\treportController:              reportController,\n\t\tvoteController:                voteController,\n\t\ttagController:                 tagController,\n\t\tfollowController:              followController,\n\t\tcollectionController:          collectionController,\n\t\tquestionController:            questionController,\n\t\tanswerController:              answerController,\n\t\tsearchController:              searchController,\n\t\trevisionController:            revisionController,\n\t\trankController:                rankController,\n\t\tadminUserController:           adminUserController,\n\t\treasonController:              reasonController,\n\t\tthemeController:               themeController,\n\t\tadminSiteInfoController:       adminSiteInfoController,\n\t\tnotificationController:        notificationController,\n\t\tsiteInfoController:            siteInfoController,\n\t\tdashboardController:           dashboardController,\n\t\tuploadController:              uploadController,\n\t\tactivityController:            activityController,\n\t\troleController:                roleController,\n\t\tpluginController:              pluginController,\n\t\tpermissionController:          permissionController,\n\t\tuserPluginController:          userPluginController,\n\t\treviewController:              reviewController,\n\t\tmetaController:                metaController,\n\t\tbadgeController:               badgeController,\n\t\tadminBadgeController:          adminBadgeController,\n\t\tapiKeyController:              apiKeyController,\n\t\taiController:                  aiController,\n\t\taiConversationController:      aiConversationController,\n\t\taiConversationAdminController: aiConversationAdminController,\n\t\tmcpController:                 mcpController,\n\t}\n}\n\nfunc (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *middleware.AuthUserMiddleware, r *gin.RouterGroup) {\n\t// i18n\n\tr.GET(\"/language/config\", a.langController.GetLangMapping)\n\tr.GET(\"/language/options\", a.langController.GetUserLangOptions)\n\n\t// siteinfo\n\tr.GET(\"/siteinfo\", a.siteInfoController.GetSiteInfo)\n\tr.GET(\"/siteinfo/legal\", a.siteInfoController.GetSiteLegalInfo)\n\n\t// user\n\tr.GET(\"/user/info\", a.userController.GetUserInfoByUserID)\n\tr.GET(\"/user/action/record\", authUserMiddleware.Auth(), a.userController.ActionRecord)\n\trouterGroup := r.Group(\"\", middleware.BanAPIForUserCenter)\n\trouterGroup.POST(\"/user/login/email\", a.userController.UserEmailLogin)\n\trouterGroup.POST(\"/user/register/email\", a.userController.UserRegisterByEmail)\n\trouterGroup.POST(\"/user/email/verification\", a.userController.UserVerifyEmail)\n\trouterGroup.PUT(\"/user/email\", a.userController.UserChangeEmailVerify)\n\trouterGroup.POST(\"/user/password/reset\", a.userController.RetrievePassWord)\n\trouterGroup.POST(\"/user/password/replacement\", a.userController.UseRePassWord)\n\trouterGroup.PUT(\"/user/notification/unsubscribe\", a.userController.UserUnsubscribeNotification)\n\n\t// plugins\n\tr.GET(\"/plugin/status\", a.pluginController.GetAllPluginStatus)\n}\n\nfunc (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {\n\t// user\n\tr.GET(\"/personal/user/info\", a.userController.GetOtherUserInfoByUsername)\n\tr.GET(\"/user/ranking\", a.userController.UserRanking)\n\tr.GET(\"/user/staff\", a.userController.UserStaff)\n\n\t// answer\n\tr.GET(\"/answer/info\", a.answerController.GetAnswerInfo)\n\tr.GET(\"/answer/page\", a.answerController.AnswerList)\n\tr.GET(\"/personal/answer/page\", a.questionController.PersonalAnswerPage)\n\n\t// question\n\tr.GET(\"/question/info\", a.questionController.GetQuestion)\n\tr.GET(\"/question/invite\", a.questionController.GetQuestionInviteUserInfo)\n\tr.GET(\"/question/page\", a.questionController.QuestionPage)\n\tr.GET(\"/question/recommend/page\", a.questionController.QuestionRecommendPage)\n\tr.GET(\"/question/similar/tag\", a.questionController.SimilarQuestion)\n\tr.GET(\"/personal/qa/top\", a.questionController.UserTop)\n\tr.GET(\"/personal/question/page\", a.questionController.PersonalQuestionPage)\n\tr.GET(\"/question/link\", a.questionController.GetQuestionLink)\n\n\t// comment\n\tr.GET(\"/comment/page\", a.commentController.GetCommentWithPage)\n\tr.GET(\"/personal/comment/page\", a.commentController.GetCommentPersonalWithPage)\n\tr.GET(\"/comment\", a.commentController.GetComment)\n\n\t// tag\n\tr.GET(\"/tags/page\", a.tagController.GetTagWithPage)\n\tr.GET(\"/tags/following\", a.tagController.GetFollowingTags)\n\tr.GET(\"/tag\", a.tagController.GetTagInfo)\n\tr.GET(\"/tags\", a.tagController.GetTagsBySlugName)\n\tr.GET(\"/tag/synonyms\", a.tagController.GetTagSynonyms)\n\n\t// search\n\tr.GET(\"/search\", a.searchController.Search)\n\tr.GET(\"/search/desc\", a.searchController.SearchDesc)\n\n\t// rank\n\tr.GET(\"/personal/rank/page\", a.rankController.GetRankPersonalWithPage)\n\n\t// reaction\n\tr.GET(\"/meta/reaction\", a.metaController.GetReaction)\n\n\t// badges\n\tr.GET(\"/badge\", a.badgeController.GetBadgeInfo)\n\tr.GET(\"/badge/awards/page\", a.badgeController.GetBadgeAwardList)\n\tr.GET(\"/badge/user/awards/recent\", a.badgeController.GetRecentBadgeAwardListByUsername)\n\tr.GET(\"/badge/user/awards\", a.badgeController.GetAllBadgeAwardListByUsername)\n\tr.GET(\"/badges\", a.badgeController.GetBadgeList)\n}\n\nfunc (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r *gin.RouterGroup) {\n\tr.GET(\"/user/logout\", a.userController.UserLogout)\n\tr.POST(\"/user/email/change/code\", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode)\n\tr.POST(\"/user/email/verification/send\", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend)\n}\n\nfunc (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {\n\t// revisions\n\tr.GET(\"/revisions\", a.revisionController.GetRevisionList)\n\tr.GET(\"/revisions/unreviewed\", a.revisionController.GetUnreviewedRevisionList)\n\tr.PUT(\"/revisions/audit\", a.revisionController.RevisionAudit)\n\tr.GET(\"/revisions/edit/check\", a.revisionController.CheckCanUpdateRevision)\n\tr.GET(\"/reviewing/type\", a.revisionController.GetReviewingType)\n\n\t// comment\n\tr.POST(\"/comment\", a.commentController.AddComment)\n\tr.DELETE(\"/comment\", a.commentController.RemoveComment)\n\tr.PUT(\"/comment\", a.commentController.UpdateComment)\n\n\t// report\n\tr.POST(\"/report\", a.reportController.AddReport)\n\tr.GET(\"/report/unreviewed/post\", a.reportController.GetUnreviewedReportPostPage)\n\tr.PUT(\"/report/review\", a.reportController.ReviewReport)\n\n\t// review\n\tr.GET(\"/review/pending/post/page\", a.reviewController.GetUnreviewedPostPage)\n\tr.PUT(\"/review/pending/post\", a.reviewController.UpdateReview)\n\n\t// vote\n\tr.POST(\"/vote/up\", a.voteController.VoteUp)\n\tr.POST(\"/vote/down\", a.voteController.VoteDown)\n\n\t// follow\n\tr.POST(\"/follow\", a.followController.Follow)\n\tr.PUT(\"/follow/tags\", a.followController.UpdateFollowTags)\n\n\t// tag\n\tr.GET(\"/question/tags\", a.tagController.SearchTagLike)\n\tr.POST(\"/tag\", a.tagController.AddTag)\n\tr.PUT(\"/tag\", a.tagController.UpdateTag)\n\tr.POST(\"/tag/recover\", a.tagController.RecoverTag)\n\tr.DELETE(\"/tag\", a.tagController.RemoveTag)\n\tr.PUT(\"/tag/synonym\", a.tagController.UpdateTagSynonym)\n\tr.POST(\"/tag/merge\", a.tagController.MergeTag)\n\n\t// collection\n\tr.POST(\"/collection/switch\", a.collectionController.CollectionSwitch)\n\tr.GET(\"/personal/collection/page\", a.questionController.PersonalCollectionPage)\n\n\t// question\n\tr.POST(\"/question\", a.questionController.AddQuestion)\n\tr.POST(\"/question/answer\", a.questionController.AddQuestionByAnswer)\n\tr.PUT(\"/question\", a.questionController.UpdateQuestion)\n\tr.PUT(\"/question/invite\", a.questionController.UpdateQuestionInviteUser)\n\tr.DELETE(\"/question\", a.questionController.RemoveQuestion)\n\tr.PUT(\"/question/status\", a.questionController.CloseQuestion)\n\tr.PUT(\"/question/operation\", a.questionController.OperationQuestion)\n\tr.PUT(\"/question/reopen\", a.questionController.ReopenQuestion)\n\tr.GET(\"/question/similar\", a.questionController.GetSimilarQuestions)\n\tr.POST(\"/question/recover\", a.questionController.QuestionRecover)\n\n\t// answer\n\tr.POST(\"/answer\", a.answerController.AddAnswer)\n\tr.PUT(\"/answer\", a.answerController.UpdateAnswer)\n\tr.POST(\"/answer/acceptance\", a.answerController.AcceptAnswer)\n\tr.DELETE(\"/answer\", a.answerController.RemoveAnswer)\n\tr.POST(\"/answer/recover\", a.answerController.RecoverAnswer)\n\n\t// user\n\tr.PUT(\"/user/password\", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord)\n\tr.PUT(\"/user/info\", a.userController.UserUpdateInfo)\n\tr.PUT(\"/user/interface\", a.userController.UserUpdateInterface)\n\tr.GET(\"/user/notification/config\", a.userController.GetUserNotificationConfig)\n\tr.PUT(\"/user/notification/config\", a.userController.UpdateUserNotificationConfig)\n\tr.GET(\"/user/info/search\", a.userController.SearchUserListByName)\n\n\t// vote\n\tr.GET(\"/personal/vote/page\", a.voteController.UserVotes)\n\n\t// reason\n\tr.GET(\"/reasons\", a.reasonController.Reasons)\n\n\t// permission\n\tr.GET(\"/permission\", a.permissionController.GetPermission)\n\n\t// notification\n\tr.GET(\"/notification/status\", a.notificationController.GetRedDot)\n\tr.PUT(\"/notification/status\", a.notificationController.ClearRedDot)\n\tr.GET(\"/notification/page\", a.notificationController.GetList)\n\tr.PUT(\"/notification/read/state/all\", a.notificationController.ClearUnRead)\n\tr.PUT(\"/notification/read/state\", a.notificationController.ClearIDUnRead)\n\n\t// upload file\n\tr.POST(\"/file\", a.uploadController.UploadFile)\n\tr.POST(\"/post/render\", a.uploadController.PostRender)\n\n\t// activity\n\tr.GET(\"/activity/timeline\", a.activityController.GetObjectTimeline)\n\tr.GET(\"/activity/timeline/detail\", a.activityController.GetObjectTimelineDetail)\n\n\t// plugin\n\tr.GET(\"/user/plugin/configs\", a.userPluginController.GetUserPluginList)\n\tr.GET(\"/user/plugin/config\", a.userPluginController.GetUserPluginConfig)\n\tr.PUT(\"/user/plugin/config\", a.userPluginController.UpdatePluginUserConfig)\n\n\t// meta\n\tr.PUT(\"/meta/reaction\", a.metaController.AddOrUpdateReaction)\n\n\t// AI chat\n\tr.POST(\"/chat/completions\", a.aiController.ChatCompletions)\n\n\t// AI conversation\n\tr.GET(\"/ai/conversation/page\", a.aiConversationController.GetConversationList)\n\tr.GET(\"/ai/conversation\", a.aiConversationController.GetConversationDetail)\n\tr.POST(\"/ai/conversation/vote\", a.aiConversationController.VoteRecord)\n}\n\nfunc (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {\n\tr.GET(\"/question/page\", a.questionController.AdminQuestionPage)\n\tr.PUT(\"/question/status\", a.questionController.AdminUpdateQuestionStatus)\n\tr.GET(\"/answer/page\", a.questionController.AdminAnswerPage)\n\tr.PUT(\"/answer/status\", a.answerController.AdminUpdateAnswerStatus)\n\n\t// user\n\tr.GET(\"/users/page\", a.adminUserController.GetUserPage)\n\tr.PUT(\"/user/status\", a.adminUserController.UpdateUserStatus)\n\tr.PUT(\"/user/role\", a.adminUserController.UpdateUserRole)\n\tr.GET(\"/user/activation\", a.adminUserController.GetUserActivation)\n\tr.POST(\"/user/activation\", a.adminUserController.SendUserActivation)\n\tr.POST(\"/user\", a.adminUserController.AddUser)\n\tr.POST(\"/users\", a.adminUserController.AddUsers)\n\tr.PUT(\"/user/password\", a.adminUserController.UpdateUserPassword)\n\tr.PUT(\"/user/profile\", a.adminUserController.EditUserProfile)\n\n\tr.DELETE(\"/delete/permanently\", a.adminUserController.DeletePermanently)\n\n\t// reason\n\tr.GET(\"/reasons\", a.reasonController.Reasons)\n\n\t// language\n\tr.GET(\"/language/options\", a.langController.GetAdminLangOptions)\n\n\t// theme\n\tr.GET(\"/theme/options\", a.themeController.GetThemeOptions)\n\n\t// siteinfo\n\tr.GET(\"/siteinfo/general\", a.adminSiteInfoController.GetGeneral)\n\tr.PUT(\"/siteinfo/general\", a.adminSiteInfoController.UpdateGeneral)\n\n\tr.GET(\"/siteinfo/interface\", a.adminSiteInfoController.GetInterface)\n\tr.PUT(\"/siteinfo/interface\", a.adminSiteInfoController.UpdateInterface)\n\tr.GET(\"/siteinfo/users-settings\", a.adminSiteInfoController.GetUsersSettings)\n\tr.PUT(\"/siteinfo/users-settings\", a.adminSiteInfoController.UpdateUsersSettings)\n\n\tr.GET(\"/siteinfo/branding\", a.adminSiteInfoController.GetSiteBranding)\n\tr.PUT(\"/siteinfo/branding\", a.adminSiteInfoController.UpdateBranding)\n\n\tr.GET(\"/siteinfo/question\", a.adminSiteInfoController.GetSiteQuestion)\n\tr.PUT(\"/siteinfo/question\", a.adminSiteInfoController.UpdateSiteQuestion)\n\tr.GET(\"/siteinfo/tag\", a.adminSiteInfoController.GetSiteTag)\n\tr.PUT(\"/siteinfo/tag\", a.adminSiteInfoController.UpdateSiteTag)\n\tr.GET(\"/siteinfo/advanced\", a.adminSiteInfoController.GetSiteAdvanced)\n\tr.PUT(\"/siteinfo/advanced\", a.adminSiteInfoController.UpdateSiteAdvanced)\n\n\tr.GET(\"/siteinfo/polices\", a.adminSiteInfoController.GetSitePolicies)\n\tr.PUT(\"/siteinfo/polices\", a.adminSiteInfoController.UpdateSitePolices)\n\tr.GET(\"/siteinfo/security\", a.adminSiteInfoController.GetSiteSecurity)\n\tr.PUT(\"/siteinfo/security\", a.adminSiteInfoController.UpdateSiteSecurity)\n\n\tr.GET(\"/siteinfo/seo\", a.adminSiteInfoController.GetSeo)\n\tr.PUT(\"/siteinfo/seo\", a.adminSiteInfoController.UpdateSeo)\n\tr.GET(\"/siteinfo/login\", a.adminSiteInfoController.GetSiteLogin)\n\tr.PUT(\"/siteinfo/login\", a.adminSiteInfoController.UpdateSiteLogin)\n\tr.GET(\"/siteinfo/custom-css-html\", a.adminSiteInfoController.GetSiteCustomCssHTML)\n\tr.PUT(\"/siteinfo/custom-css-html\", a.adminSiteInfoController.UpdateSiteCustomCssHTML)\n\tr.GET(\"/siteinfo/theme\", a.adminSiteInfoController.GetSiteTheme)\n\tr.PUT(\"/siteinfo/theme\", a.adminSiteInfoController.SaveSiteTheme)\n\tr.GET(\"/siteinfo/users\", a.adminSiteInfoController.GetSiteUsers)\n\tr.PUT(\"/siteinfo/users\", a.adminSiteInfoController.UpdateSiteUsers)\n\tr.GET(\"/setting/smtp\", a.adminSiteInfoController.GetSMTPConfig)\n\tr.PUT(\"/setting/smtp\", a.adminSiteInfoController.UpdateSMTPConfig)\n\tr.GET(\"/setting/privileges\", a.adminSiteInfoController.GetPrivilegesConfig)\n\tr.PUT(\"/setting/privileges\", a.adminSiteInfoController.UpdatePrivilegesConfig)\n\n\t// dashboard\n\tr.GET(\"/dashboard\", a.dashboardController.DashboardInfo)\n\n\t// roles\n\tr.GET(\"/roles\", a.roleController.GetRoleList)\n\n\t// plugin\n\tr.GET(\"/plugins\", a.pluginController.GetPluginList)\n\tr.PUT(\"/plugin/status\", a.pluginController.UpdatePluginStatus)\n\tr.GET(\"/plugin/config\", a.pluginController.GetPluginConfig)\n\tr.PUT(\"/plugin/config\", a.pluginController.UpdatePluginConfig)\n\n\t// badge\n\tr.GET(\"/badges\", a.adminBadgeController.GetBadgeList)\n\tr.PUT(\"/badge/status\", a.adminBadgeController.UpdateBadgeStatus)\n\n\t// api key\n\tr.GET(\"/api-key/all\", a.apiKeyController.GetAllAPIKeys)\n\tr.POST(\"/api-key\", a.apiKeyController.AddAPIKey)\n\tr.PUT(\"/api-key\", a.apiKeyController.UpdateAPIKey)\n\tr.DELETE(\"/api-key\", a.apiKeyController.DeleteAPIKey)\n\n\t// ai config\n\tr.GET(\"/ai-config\", a.adminSiteInfoController.GetAIConfig)\n\tr.PUT(\"/ai-config\", a.adminSiteInfoController.UpdateAIConfig)\n\tr.GET(\"/ai-provider\", a.adminSiteInfoController.GetAIProvider)\n\tr.POST(\"/ai-models\", a.adminSiteInfoController.RequestAIModels)\n\n\t// mcp config\n\tr.GET(\"/mcp-config\", a.adminSiteInfoController.GetMCPConfig)\n\tr.PUT(\"/mcp-config\", a.adminSiteInfoController.UpdateMCPConfig)\n\n\t// AI conversation management\n\tr.GET(\"/ai/conversation/page\", a.aiConversationAdminController.GetConversationList)\n\tr.GET(\"/ai/conversation\", a.aiConversationAdminController.GetConversationDetail)\n\tr.DELETE(\"/ai/conversation\", a.aiConversationAdminController.DeleteConversation)\n}\n"
  },
  {
    "path": "internal/router/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\n// SwaggerConfig struct describes configure for the Swagger API endpoint\ntype SwaggerConfig struct {\n\tShow     bool   `json:\"show\" mapstructure:\"show\" yaml:\"show\"`\n\tProtocol string `json:\"protocol\" mapstructure:\"protocol\" yaml:\"protocol\"`\n\tHost     string `json:\"host\" mapstructure:\"host\" yaml:\"host\"`\n\tAddress  string `json:\"address\" mapstructure:\"address\" yaml:\"address\"`\n}\n"
  },
  {
    "path": "internal/router/mcp_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"github.com/apache/answer/internal/schema/mcp_tools\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/mark3labs/mcp-go/server\"\n)\n\nfunc (a *AnswerAPIRouter) RegisterMCPRouter(r *gin.RouterGroup) {\n\ts := server.NewMCPServer(\"Answer Enterprise MCP Server\", \"1.0.0\")\n\n\ts.AddTool(mcp_tools.NewQuestionsTool(), a.mcpController.MCPQuestionsHandler())\n\ts.AddTool(mcp_tools.NewAnswersTool(), a.mcpController.MCPAnswersHandler())\n\ts.AddTool(mcp_tools.NewCommentsTool(), a.mcpController.MCPCommentsHandler())\n\ts.AddTool(mcp_tools.NewTagsTool(), a.mcpController.MCPTagsHandler())\n\ts.AddTool(mcp_tools.NewTagDetailTool(), a.mcpController.MCPTagDetailsHandler())\n\ts.AddTool(mcp_tools.NewUserTool(), a.mcpController.MCPUserDetailsHandler())\n\n\tsseServer := server.NewSSEServer(s,\n\t\tserver.WithSSEEndpoint(\"/answer/api/v1/mcp/see\"),\n\t\tserver.WithMessageEndpoint(\"/answer/api/v1/mcp/message\"),\n\t)\n\tr.GET(\"/mcp/sse\", gin.WrapH(sseServer.SSEHandler()))\n\tr.POST(\"/mcp/message\", gin.WrapH(sseServer.MessageHandler()))\n}\n"
  },
  {
    "path": "internal/router/plugin_api_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"github.com/apache/answer/internal/controller\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype PluginAPIRouter struct {\n\tconnectorController  *controller.ConnectorController\n\tuserCenterController *controller.UserCenterController\n\tcaptchaController    *controller.CaptchaController\n\tembedController      *controller.EmbedController\n\trenderController     *controller.RenderController\n\tsidebarController    *controller.SidebarController\n}\n\nfunc NewPluginAPIRouter(\n\tconnectorController *controller.ConnectorController,\n\tuserCenterController *controller.UserCenterController,\n\tcaptchaController *controller.CaptchaController,\n\tembedController *controller.EmbedController,\n\trenderController *controller.RenderController,\n\tsidebarController *controller.SidebarController,\n) *PluginAPIRouter {\n\treturn &PluginAPIRouter{\n\t\tconnectorController:  connectorController,\n\t\tuserCenterController: userCenterController,\n\t\tcaptchaController:    captchaController,\n\t\tembedController:      embedController,\n\t\trenderController:     renderController,\n\t\tsidebarController:    sidebarController,\n\t}\n}\n\nfunc (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {\n\t// connector plugin\n\tconnectorController := pr.connectorController\n\tr.GET(controller.ConnectorLoginRouterPrefix+\":name\", connectorController.ConnectorLoginDispatcher)\n\tr.GET(controller.ConnectorRedirectRouterPrefix+\":name\", connectorController.ConnectorRedirectDispatcher)\n\tr.GET(\"/connector/info\", connectorController.ConnectorsInfo)\n\tr.POST(\"/connector/binding/email\", connectorController.ExternalLoginBindingUserSendEmail)\n\n\t// user center plugin\n\tr.GET(\"/user-center/agent\", pr.userCenterController.UserCenterAgent)\n\tr.GET(\"/user-center/personal/branding\", pr.userCenterController.UserCenterPersonalBranding)\n\tr.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect)\n\tr.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect)\n\tr.GET(\"/user-center/login/callback\", pr.userCenterController.UserCenterLoginCallback)\n\tr.GET(\"/user-center/sign-up/callback\", pr.userCenterController.UserCenterSignUpCallback)\n\n\t// captcha plugin\n\tr.GET(\"/captcha/config\", pr.captchaController.GetCaptchaConfig)\n\tr.GET(\"/embed/config\", pr.embedController.GetEmbedConfig)\n\tr.GET(\"/render/config\", pr.renderController.GetRenderConfig)\n\n\t// sidebar plugin\n\tr.GET(\"/sidebar/config\", pr.sidebarController.GetSidebarConfig)\n}\n\nfunc (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {\n\tconnectorController := pr.connectorController\n\tr.GET(\"/connector/user/info\", connectorController.ConnectorsUserInfo)\n\tr.DELETE(\"/connector/user/unbinding\", connectorController.ExternalLoginUnbinding)\n\n\tr.GET(\"/user-center/user/settings\", pr.userCenterController.UserCenterUserSettings)\n}\n\nfunc (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) {\n\tr.GET(\"/user-center/agent\", pr.userCenterController.UserCenterAdminFunctionAgent)\n}\n"
  },
  {
    "path": "internal/router/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport \"github.com/google/wire\"\n\n// ProviderSetRouter is providers.\nvar ProviderSetRouter = wire.NewSet(\n\tNewAnswerAPIRouter,\n\tNewSwaggerRouter,\n\tNewStaticRouter,\n\tNewUIRouter,\n\tNewTemplateRouter,\n\tNewPluginAPIRouter,\n)\n"
  },
  {
    "path": "internal/router/static_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// StaticRouter static api router\ntype StaticRouter struct {\n\tserviceConfig *service_config.ServiceConfig\n}\n\n// NewStaticRouter new static api router\nfunc NewStaticRouter(serviceConfig *service_config.ServiceConfig) *StaticRouter {\n\treturn &StaticRouter{\n\t\tserviceConfig: serviceConfig,\n\t}\n}\n\n// RegisterStaticRouter register static api router\nfunc (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) {\n\tr.Static(\"/uploads/\"+constant.AvatarSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarSubPath))\n\tr.Static(\"/uploads/\"+constant.AvatarThumbSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarThumbSubPath))\n\tr.Static(\"/uploads/\"+constant.PostSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.PostSubPath))\n\tr.Static(\"/uploads/\"+constant.BrandingSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.BrandingSubPath))\n\tr.GET(\"/uploads/\"+constant.FilesPostSubPath+\"/*filepath\", func(c *gin.Context) {\n\t\t// The filepath such as hash/123.pdf\n\t\tfilePath := c.Param(\"filepath\")\n\t\t// The original filename is 123.pdf\n\t\toriginalFilename := filepath.Base(filePath)\n\t\t// The real filename is hash.pdf\n\t\trealFilename := strings.TrimSuffix(filePath, \"/\"+originalFilename) + filepath.Ext(originalFilename)\n\t\t// The file local path is /uploads/files/post/hash.pdf\n\t\tfileLocalPath := filepath.Join(a.serviceConfig.UploadPath, constant.FilesPostSubPath, realFilename)\n\t\t// If the file is not exist, return 404\n\t\tif !dir.CheckFileExist(fileLocalPath) {\n\t\t\tc.Redirect(http.StatusFound, \"/404\")\n\t\t\treturn\n\t\t}\n\t\tc.FileAttachment(fileLocalPath, originalFilename)\n\t})\n}\n"
  },
  {
    "path": "internal/router/swagger_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/apache/answer/docs\"\n\t\"github.com/gin-gonic/gin\"\n\tswaggerfiles \"github.com/swaggo/files\"\n\tginSwagger \"github.com/swaggo/gin-swagger\"\n)\n\n// SwaggerRouter swagger api router\ntype SwaggerRouter struct {\n\tconfig *SwaggerConfig\n}\n\n// NewSwaggerRouter new swagger api router\nfunc NewSwaggerRouter(config *SwaggerConfig) *SwaggerRouter {\n\treturn &SwaggerRouter{\n\t\tconfig: config,\n\t}\n}\n\n// Register register swagger api router\nfunc (a *SwaggerRouter) Register(r *gin.RouterGroup) {\n\tif a.config.Show {\n\t\ta.InitSwaggerDocs()\n\t\tgofmt := fmt.Sprintf(\"%s://%s%s/swagger/doc.json\", a.config.Protocol, a.config.Host, a.config.Address)\n\t\tr.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerfiles.Handler, ginSwagger.URL(gofmt)))\n\t}\n}\n\n// InitSwaggerDocs init swagger docs\nfunc (a *SwaggerRouter) InitSwaggerDocs() {\n\tdocs.SwaggerInfo.Host = fmt.Sprintf(\"%s%s\", a.config.Host, a.config.Address)\n}\n"
  },
  {
    "path": "internal/router/template_router.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"github.com/apache/answer/internal/base/middleware\"\n\t\"github.com/apache/answer/internal/controller\"\n\ttemplaterender \"github.com/apache/answer/internal/controller/template_render\"\n\t\"github.com/apache/answer/internal/controller_admin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype TemplateRouter struct {\n\ttemplateController       *controller.TemplateController\n\ttemplateRenderController *templaterender.TemplateRenderController\n\tsiteInfoController       *controller_admin.SiteInfoController\n\tauthUserMiddleware       *middleware.AuthUserMiddleware\n}\n\nfunc NewTemplateRouter(\n\ttemplateController *controller.TemplateController,\n\ttemplateRenderController *templaterender.TemplateRenderController,\n\tsiteInfoController *controller_admin.SiteInfoController,\n\tauthUserMiddleware *middleware.AuthUserMiddleware,\n\n) *TemplateRouter {\n\treturn &TemplateRouter{\n\t\ttemplateController:       templateController,\n\t\ttemplateRenderController: templateRenderController,\n\t\tsiteInfoController:       siteInfoController,\n\t\tauthUserMiddleware:       authUserMiddleware,\n\t}\n}\n\n// RegisterTemplateRouter template router\nfunc (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup, baseURLPath string) {\n\tseoNoAuth := r.Group(baseURLPath)\n\tseoNoAuth.GET(\"/sitemap.xml\", a.templateController.Sitemap)\n\tseoNoAuth.GET(\"/sitemap/:page\", a.templateController.SitemapPage)\n\n\tseoNoAuth.GET(\"/robots.txt\", a.siteInfoController.GetRobots)\n\tseoNoAuth.GET(\"/custom.css\", a.siteInfoController.GetCss)\n\n\tseoNoAuth.GET(\"/404\", a.templateController.Page404)\n\n\tseoNoAuth.GET(\"/opensearch.xml\", a.templateController.OpenSearch)\n\n\tseo := r.Group(baseURLPath)\n\tseo.Use(a.authUserMiddleware.CheckPrivateMode())\n\tseo.GET(\"/\", a.templateController.Index)\n\tseo.GET(\"/questions\", a.templateController.QuestionList)\n\tseo.GET(\"/questions/:id\", a.templateController.QuestionInfo)\n\tseo.GET(\"/questions/:id/:title\", a.templateController.QuestionInfo)\n\tseo.GET(\"/questions/:id/:title/:answerid\", a.templateController.QuestionInfo)\n\tseo.GET(\"/tags\", a.templateController.TagList)\n\tseo.GET(\"/tags/:tag\", a.templateController.TagInfo)\n\tseo.GET(\"/users/:username\", a.templateController.UserInfo)\n}\n"
  },
  {
    "path": "internal/router/ui.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/controller\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/apache/answer/ui\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nconst UIIndexFilePath = \"build/index.html\"\nconst UIRootFilePath = \"build\"\nconst UIStaticPath = \"build/static\"\n\n// UIRouter is an interface that provides ui static file routers\ntype UIRouter struct {\n\tsiteInfoController *controller.SiteInfoController\n\tsiteInfoService    siteinfo_common.SiteInfoCommonService\n}\n\n// NewUIRouter creates a new UIRouter instance with the embed resources\nfunc NewUIRouter(\n\tsiteInfoController *controller.SiteInfoController,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n) *UIRouter {\n\treturn &UIRouter{\n\t\tsiteInfoController: siteInfoController,\n\t\tsiteInfoService:    siteInfoService,\n\t}\n}\n\n// _resource is an interface that provides static file, it's a private interface\ntype _resource struct {\n\tfs embed.FS\n}\n\n// Open to implement the interface by http.FS required\nfunc (r *_resource) Open(name string) (fs.File, error) {\n\tname = fmt.Sprintf(UIStaticPath+\"/%s\", name)\n\tlog.Debugf(\"open static path %s\", name)\n\treturn r.fs.Open(name)\n}\n\n// Register a new static resource which generated by ui directory\nfunc (a *UIRouter) Register(r *gin.Engine, baseURLPath string) {\n\tstaticPath := os.Getenv(\"ANSWER_STATIC_PATH\")\n\n\t// if ANSWER_STATIC_PATH is set and not empty, ignore embed resource\n\tif staticPath != \"\" {\n\t\tinfo, err := os.Stat(staticPath)\n\n\t\tif err != nil || !info.IsDir() {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\tlog.Debugf(\"registering static path %s\", staticPath)\n\n\t\t\tr.LoadHTMLGlob(staticPath + \"/*.html\")\n\t\t\tr.Static(baseURLPath+\"/static\", staticPath+\"/static\")\n\t\t\tr.NoRoute(func(c *gin.Context) {\n\t\t\t\tc.HTML(http.StatusOK, \"index.html\", gin.H{})\n\t\t\t})\n\n\t\t\t// return immediately if the static path is set\n\t\t\treturn\n\t\t}\n\t}\n\n\t// handle the static file by default ui static files\n\tr.StaticFS(baseURLPath+\"/static\", http.FS(&_resource{\n\t\tfs: ui.Build,\n\t}))\n\n\t// specify the not router for default routes and redirect\n\tr.NoRoute(func(c *gin.Context) {\n\t\turlPath := c.Request.URL.Path\n\t\tif len(baseURLPath) > 0 {\n\t\t\turlPath = strings.TrimPrefix(urlPath, baseURLPath)\n\t\t}\n\t\tfilePath := \"\"\n\t\tswitch urlPath {\n\t\tcase \"/favicon.ico\":\n\t\t\tbranding, err := a.siteInfoService.GetSiteBranding(c)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tif branding != nil && branding.Favicon != \"\" {\n\t\t\t\tc.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))\n\t\t\t\treturn\n\t\t\t} else if branding != nil && branding.SquareIcon != \"\" {\n\t\t\t\tc.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon))\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tc.Header(\"content-type\", \"image/vnd.microsoft.icon\")\n\t\t\t\tfilePath = UIRootFilePath + urlPath\n\t\t\t}\n\t\tcase \"/manifest.json\":\n\t\t\ta.siteInfoController.GetManifestJson(c)\n\t\t\treturn\n\t\tcase \"/install\":\n\t\t\t// if answer is running by run command user can not access install page.\n\t\t\tc.Redirect(http.StatusFound, \"/\")\n\t\t\treturn\n\t\tdefault:\n\t\t\tfilePath = UIIndexFilePath\n\t\t\tc.Header(\"content-type\", \"text/html;charset=utf-8\")\n\t\t\tc.Header(\"X-Frame-Options\", \"DENY\")\n\t\t}\n\t\tfile, err := ui.Build.ReadFile(filePath)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tc.Status(http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tcdnPrefix := \"\"\n\t\t_ = plugin.CallCDN(func(fn plugin.CDN) error {\n\t\t\tcdnPrefix = fn.GetStaticPrefix()\n\t\t\treturn nil\n\t\t})\n\t\tif cdnPrefix != \"\" {\n\t\t\tif cdnPrefix[len(cdnPrefix)-1:] == \"/\" {\n\t\t\t\tcdnPrefix = strings.TrimSuffix(cdnPrefix, \"/\")\n\t\t\t}\n\t\t\tc.String(http.StatusOK, strings.ReplaceAll(string(file), \"/static\", cdnPrefix+\"/static\"))\n\t\t\treturn\n\t\t}\n\n\t\t// This part is to solve the problem of returning 404 when the access path does not exist.\n\t\t// However, there is no way to check whether the current route exists in the frontend.\n\t\t// We can only hand over the route to the frontend for processing.\n\t\t// And the plugin, frontend routes can now be dynamically registered,\n\t\t// so there's no good way to get all frontend routes\n\t\t//if filePath == UIIndexFilePath {\n\t\t//\tc.String(http.StatusNotFound, string(file))\n\t\t//\treturn\n\t\t//}\n\n\t\tc.String(http.StatusOK, string(file))\n\t})\n}\n"
  },
  {
    "path": "internal/router/ui_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage router\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/service/mock\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestUIRouter_FaviconWithNilBranding(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdefer ctrl.Finish()\n\n\tmockSiteInfoService := mock.NewMockSiteInfoCommonService(ctrl)\n\n\t// Simulate a database error\n\tmockSiteInfoService.EXPECT().\n\t\tGetSiteBranding(gomock.Any()).\n\t\tReturn(nil, errors.New(\"database connection failed\"))\n\n\trouter := &UIRouter{\n\t\tsiteInfoService: mockSiteInfoService,\n\t}\n\n\tgin.SetMode(gin.TestMode)\n\tr := gin.New()\n\trouter.Register(r, \"\")\n\n\treq := httptest.NewRequest(http.MethodGet, \"/favicon.ico\", nil)\n\tw := httptest.NewRecorder()\n\n\tr.ServeHTTP(w, req)\n\n\tassert.Equal(t, http.StatusOK, w.Code)\n}\n"
  },
  {
    "path": "internal/schema/activity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"github.com/apache/answer/internal/base/constant\"\n\n// ActivityMsg activity message\ntype ActivityMsg struct {\n\tUserID           string\n\tTriggerUserID    int64\n\tObjectID         string\n\tOriginalObjectID string\n\tActivityTypeKey  constant.ActivityTypeKey\n\tRevisionID       string\n\tExtraInfo        map[string]string\n}\n\n// GetObjectTimelineReq get object timeline request\ntype GetObjectTimelineReq struct {\n\tObjectID string `validate:\"omitempty,gt=0,lte=100\" form:\"object_id\"`\n\tShowVote bool   `validate:\"omitempty\" form:\"show_vote\"`\n\tUserID   string `json:\"-\"`\n\tIsAdmin  bool   `json:\"-\"`\n}\n\n// GetObjectTimelineResp get object timeline response\ntype GetObjectTimelineResp struct {\n\tObjectInfo *ActObjectInfo       `json:\"object_info\"`\n\tTimeline   []*ActObjectTimeline `json:\"timeline\"`\n}\n\n// ActObjectTimeline act object timeline\ntype ActObjectTimeline struct {\n\tActivityID   string         `json:\"activity_id\"`\n\tRevisionID   string         `json:\"revision_id\"`\n\tCreatedAt    int64          `json:\"created_at\"`\n\tActivityType string         `json:\"activity_type\"`\n\tComment      string         `json:\"comment\"`\n\tObjectID     string         `json:\"object_id\"`\n\tObjectType   string         `json:\"object_type\"`\n\tCancelled    bool           `json:\"cancelled\"`\n\tCancelledAt  int64          `json:\"cancelled_at\"`\n\tUserInfo     *UserBasicInfo `json:\"user_info,omitempty\"`\n}\n\n// ActObjectInfo act object info\ntype ActObjectInfo struct {\n\tTitle           string `json:\"title\"`\n\tObjectType      string `json:\"object_type\"`\n\tQuestionID      string `json:\"question_id\"`\n\tAnswerID        string `json:\"answer_id\"`\n\tUsername        string `json:\"username\"`\n\tDisplayName     string `json:\"display_name\"`\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n}\n\n// GetObjectTimelineDetailReq get object timeline detail request\ntype GetObjectTimelineDetailReq struct {\n\tNewRevisionID string `validate:\"required,gt=0,lte=100\" form:\"new_revision_id\"`\n\tOldRevisionID string `validate:\"required,gt=0,lte=100\" form:\"old_revision_id\"`\n\tUserID        string `json:\"-\"`\n}\n\n// GetObjectTimelineDetailResp get object timeline detail response\ntype GetObjectTimelineDetailResp struct {\n\tNewRevision *ObjectTimelineDetail `json:\"new_revision\"`\n\tOldRevision *ObjectTimelineDetail `json:\"old_revision\"`\n}\n\n// ObjectTimelineDetail object timeline detail\ntype ObjectTimelineDetail struct {\n\tTitle           string               `json:\"title\"`\n\tTags            []*ObjectTimelineTag `json:\"tags\"`\n\tOriginalText    string               `json:\"original_text\"`\n\tSlugName        string               `json:\"slug_name\"`\n\tMainTagSlugName string               `json:\"main_tag_slug_name\"`\n}\n\n// ObjectTimelineTag object timeline tags\ntype ObjectTimelineTag struct {\n\tSlugName        string `json:\"slug_name\"`\n\tDisplayName     string `json:\"display_name\"`\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n\tRecommend       bool   `json:\"recommend\"`\n\tReserved        bool   `json:\"reserved\"`\n}\n\n// PassReviewActivity pass review activity\ntype PassReviewActivity struct {\n\tUserID           string `json:\"user_id\"`\n\tTriggerUserID    string `json:\"trigger_user_id\"`\n\tObjectID         string `json:\"object_id\"`\n\tOriginalObjectID string `json:\"original_object_id\"`\n\tRevisionID       string `json:\"revision_id\"`\n}\n"
  },
  {
    "path": "internal/schema/ai_config_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// GetAIProviderResp get AI providers response\ntype GetAIProviderResp struct {\n\tName           string `json:\"name\"`\n\tDisplayName    string `json:\"display_name\"`\n\tDefaultAPIHost string `json:\"default_api_host\"`\n}\n\n// GetAIModelsResp get AI model response\ntype GetAIModelsResp struct {\n\tObject string `json:\"object\"`\n\tData   []struct {\n\t\tId      string `json:\"id\"`\n\t\tObject  string `json:\"object\"`\n\t\tCreated int    `json:\"created\"`\n\t\tOwnedBy string `json:\"owned_by\"`\n\t} `json:\"data\"`\n}\n\ntype GetAIModelsReq struct {\n\tAPIHost string `json:\"api_host\"`\n\tAPIKey  string `json:\"api_key\"`\n}\n\n// GetAIModelResp get AI model response\ntype GetAIModelResp struct {\n\tId      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int    `json:\"created\"`\n\tOwnedBy string `json:\"owned_by\"`\n}\n"
  },
  {
    "path": "internal/schema/ai_conversation_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/validator\"\n)\n\n// AIConversationListReq ai conversation list req\ntype AIConversationListReq struct {\n\tPage     int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tUserID   string `validate:\"omitempty\" json:\"-\"`\n}\n\n// AIConversationListItem ai conversation list item\ntype AIConversationListItem struct {\n\tConversationID string `json:\"conversation_id\"`\n\tTopic          string `json:\"topic\"`\n\tCreatedAt      int64  `json:\"created_at\"`\n}\n\n// AIConversationDetailReq ai conversation detail req\ntype AIConversationDetailReq struct {\n\tConversationID string `validate:\"required\" form:\"conversation_id\" json:\"conversation_id\"`\n\tUserID         string `validate:\"omitempty\" json:\"-\"`\n}\n\n// AIConversationRecord ai conversation record\ntype AIConversationRecord struct {\n\tChatCompletionID string `json:\"chat_completion_id\"`\n\tRole             string `json:\"role\"`\n\tContent          string `json:\"content\"`\n\tHelpful          int    `json:\"helpful\"`\n\tUnhelpful        int    `json:\"unhelpful\"`\n\tCreatedAt        int64  `json:\"created_at\"`\n}\n\n// AIConversationDetailResp ai conversation detail resp\ntype AIConversationDetailResp struct {\n\tConversationID string                  `json:\"conversation_id\"`\n\tTopic          string                  `json:\"topic\"`\n\tRecords        []*AIConversationRecord `json:\"records\"`\n\tCreatedAt      int64                   `json:\"created_at\"`\n\tUpdatedAt      int64                   `json:\"updated_at\"`\n}\n\n// AIConversationVoteReq ai conversation vote req\ntype AIConversationVoteReq struct {\n\tChatCompletionID string `validate:\"required\" json:\"chat_completion_id\"`\n\tVoteType         string `validate:\"required,oneof=helpful unhelpful\" json:\"vote_type\"`\n\tCancel           bool   `validate:\"omitempty\" json:\"cancel\"`\n\tUserID           string `validate:\"omitempty\" json:\"-\"`\n}\n\n// AIConversationAdminListReq ai conversation admin list req\ntype AIConversationAdminListReq struct {\n\tPage     int `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n}\n\n// AIConversationAdminListItem ai conversation admin list item\ntype AIConversationAdminListItem struct {\n\tID             string                 `json:\"id\"`\n\tTopic          string                 `json:\"topic\"`\n\tUserInfo       AIConversationUserInfo `json:\"user_info\"`\n\tHelpfulCount   int64                  `json:\"helpful_count\"`\n\tUnhelpfulCount int64                  `json:\"unhelpful_count\"`\n\tCreatedAt      int64                  `json:\"created_at\"`\n}\n\n// AIConversationUserInfo ai conversation user info\ntype AIConversationUserInfo struct {\n\tID          string `json:\"id\"`\n\tUsername    string `json:\"username\"`\n\tDisplayName string `json:\"display_name\"`\n\tAvatar      string `json:\"avatar\"`\n\tRank        int    `json:\"rank\"`\n}\n\n// AIConversationAdminDetailReq ai conversation admin detail req\ntype AIConversationAdminDetailReq struct {\n\tConversationID string `validate:\"required\" form:\"conversation_id\" json:\"conversation_id\"`\n}\n\n// AIConversationAdminDetailResp ai conversation admin detail resp\ntype AIConversationAdminDetailResp struct {\n\tConversationID string                 `json:\"conversation_id\"`\n\tTopic          string                 `json:\"topic\"`\n\tUserInfo       AIConversationUserInfo `json:\"user_info\"`\n\tRecords        []AIConversationRecord `json:\"records\"`\n\tCreatedAt      int64                  `json:\"created_at\"`\n}\n\n// AIConversationAdminDeleteReq admin delete ai\ntype AIConversationAdminDeleteReq struct {\n\tConversationID string `validate:\"required\" json:\"conversation_id\"`\n}\n\nfunc (req *AIConversationDetailReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treturn nil, nil\n}\n\nfunc (req *AIConversationVoteReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treturn nil, nil\n}\n"
  },
  {
    "path": "internal/schema/answer_activity_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// AcceptAnswerOperationInfo accept answer operation info\ntype AcceptAnswerOperationInfo struct {\n\tTriggerUserID    string\n\tQuestionObjectID string\n\tQuestionUserID   string\n\tAnswerObjectID   string\n\tAnswerUserID     string\n\n\t// vote activity info\n\tActivities []*AcceptAnswerActivity\n}\n\n// AcceptAnswerActivity accept answer activity\ntype AcceptAnswerActivity struct {\n\tActivityType     int\n\tActivityUserID   string\n\tTriggerUserID    string\n\tOriginalObjectID string\n\tRank             int\n}\n\nfunc (v *AcceptAnswerActivity) HasRank() int {\n\tif v.Rank != 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc (a *AcceptAnswerOperationInfo) GetUserIDs() (userIDs []string) {\n\tfor _, act := range a.Activities {\n\t\tuserIDs = append(userIDs, act.ActivityUserID)\n\t}\n\treturn userIDs\n}\n"
  },
  {
    "path": "internal/schema/answer_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// RemoveAnswerReq delete answer request\ntype RemoveAnswerReq struct {\n\tID          string `validate:\"required\" json:\"id\"`\n\tUserID      string `json:\"-\"`\n\tCanDelete   bool   `json:\"-\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\n// RecoverAnswerReq recover answer request\ntype RecoverAnswerReq struct {\n\tAnswerID string `validate:\"required\" json:\"answer_id\"`\n\tUserID   string `json:\"-\"`\n}\n\nconst (\n\tAnswerAcceptedFailed = 1\n\tAnswerAcceptedEnable = 2\n)\n\ntype AnswerAddReq struct {\n\tQuestionID  string `json:\"question_id\"`\n\tContent     string `validate:\"required,notblank,gte=6,lte=65535\" json:\"content\"`\n\tHTML        string `json:\"-\"`\n\tUserID      string `json:\"-\"`\n\tCanEdit     bool   `json:\"-\"`\n\tCanDelete   bool   `json:\"-\"`\n\tCanRecover  bool   `json:\"-\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n\tIP          string `json:\"-\"`\n\tUserAgent   string `json:\"-\"`\n}\n\nfunc (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.HTML = converter.Markdown2HTML(req.Content)\n\tif req.HTML == \"\" {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"content\",\n\t\t\tErrorMsg:   reason.AnswerContentCannotEmpty,\n\t\t}), errors.BadRequest(reason.AnswerContentCannotEmpty)\n\t}\n\treturn nil, nil\n}\n\ntype GetAnswerInfoResp struct {\n\tInfo     *AnswerInfo       `json:\"info\"`\n\tQuestion *QuestionInfoResp `json:\"question\"`\n}\n\ntype AnswerUpdateReq struct {\n\tID           string `json:\"id\"`\n\tTitle        string `json:\"title\"`\n\tContent      string `validate:\"required,notblank,gte=6,lte=65535\" json:\"content\"`\n\tEditSummary  string `validate:\"omitempty\" json:\"edit_summary\"`\n\tHTML         string `json:\"-\"`\n\tUserID       string `json:\"-\"`\n\tNoNeedReview bool   `json:\"-\"`\n\tCanEdit      bool   `json:\"-\"`\n\tCaptchaID    string `json:\"captcha_id\"`\n\tCaptchaCode  string `json:\"captcha_code\"`\n}\n\nfunc (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.HTML = converter.Markdown2HTML(req.Content)\n\tif req.HTML == \"\" {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"content\",\n\t\t\tErrorMsg:   reason.AnswerContentCannotEmpty,\n\t\t}), errors.BadRequest(reason.AnswerContentCannotEmpty)\n\t}\n\treturn nil, nil\n}\n\n// AnswerUpdateResp answer update resp\ntype AnswerUpdateResp struct {\n\tWaitForReview bool `json:\"wait_for_review\"`\n}\n\ntype AnswerListReq struct {\n\tQuestionID string `json:\"question_id\" form:\"question_id\"`\n\tOrder      string `json:\"order\" form:\"order\"`\n\tPage       int    `json:\"page\" form:\"page\"`\n\tPageSize   int    `json:\"page_size\" form:\"page_size\"`\n\tUserID     string `json:\"-\"`\n\tIsAdmin    bool   `json:\"-\"`\n\tCanEdit    bool   `json:\"-\"`\n\tCanDelete  bool   `json:\"-\"`\n\tCanRecover bool   `json:\"-\"`\n}\n\ntype AnswerInfo struct {\n\tID             string            `json:\"id\"`\n\tQuestionID     string            `json:\"question_id\"`\n\tContent        string            `json:\"content\"`\n\tHTML           string            `json:\"html\"`\n\tCreateTime     int64             `json:\"create_time\"`\n\tUpdateTime     int64             `json:\"update_time\"`\n\tAccepted       int               `json:\"accepted\"`\n\tUserID         string            `json:\"-\"`\n\tUpdateUserID   string            `json:\"-\"`\n\tUserInfo       *UserBasicInfo    `json:\"user_info,omitempty\"`\n\tUpdateUserInfo *UserBasicInfo    `json:\"update_user_info,omitempty\"`\n\tCollected      bool              `json:\"collected\"`\n\tVoteStatus     string            `json:\"vote_status\"`\n\tVoteCount      int               `json:\"vote_count\"`\n\tQuestionInfo   *QuestionInfoResp `json:\"question_info,omitempty\"`\n\tStatus         int               `json:\"status\"`\n\n\t// MemberActions\n\tMemberActions []*PermissionMemberAction `json:\"member_actions\"`\n}\n\ntype AdminAnswerInfo struct {\n\tID           string         `json:\"id\"`\n\tQuestionID   string         `json:\"question_id\"`\n\tDescription  string         `json:\"description\"`\n\tCreateTime   int64          `json:\"create_time\"`\n\tUpdateTime   int64          `json:\"update_time\"`\n\tAccepted     int            `json:\"accepted\"`\n\tUserID       string         `json:\"-\"`\n\tUpdateUserID string         `json:\"-\"`\n\tUserInfo     *UserBasicInfo `json:\"user_info\"`\n\tVoteCount    int            `json:\"vote_count\"`\n\tQuestionInfo struct {\n\t\tTitle string `json:\"title\"`\n\t} `json:\"question_info\"`\n}\n\ntype AcceptAnswerReq struct {\n\tQuestionID string `validate:\"required,gt=0,lte=30\" json:\"question_id\"`\n\tAnswerID   string `validate:\"omitempty\" json:\"answer_id\"`\n\tUserID     string `json:\"-\"`\n}\n\nfunc (req *AcceptAnswerReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tif len(req.AnswerID) == 0 {\n\t\treq.AnswerID = \"0\"\n\t}\n\treturn nil, nil\n}\n\ntype AdminUpdateAnswerStatusReq struct {\n\tAnswerID string `validate:\"required\" json:\"answer_id\"`\n\tStatus   string `validate:\"required,oneof=available deleted\" json:\"status\"`\n\tUserID   string `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/schema/api_key_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// GetAPIKeyReq get api key request\ntype GetAPIKeyReq struct {\n\tUserID string `json:\"-\"`\n}\n\n// GetAPIKeyResp get api keys response\ntype GetAPIKeyResp struct {\n\tID          int    `json:\"id\"`\n\tAccessKey   string `json:\"access_key\"`\n\tDescription string `json:\"description\"`\n\tScope       string `json:\"scope\"`\n\tCreatedAt   int64  `json:\"created_at\"`\n\tLastUsedAt  int64  `json:\"last_used_at\"`\n}\n\n// AddAPIKeyReq add api key request\ntype AddAPIKeyReq struct {\n\tDescription string `validate:\"required,notblank,lte=150\" json:\"description\"`\n\tScope       string `validate:\"required,oneof=read-only global\" json:\"scope\"`\n\tUserID      string `json:\"-\"`\n}\n\n// AddAPIKeyResp add api key response\ntype AddAPIKeyResp struct {\n\tAccessKey string `json:\"access_key\"`\n}\n\n// UpdateAPIKeyReq update api key request\ntype UpdateAPIKeyReq struct {\n\tID          int    `validate:\"required\" json:\"id\"`\n\tDescription string `validate:\"required,notblank,lte=150\" json:\"description\"`\n\tUserID      string `json:\"-\"`\n}\n\n// DeleteAPIKeyReq delete api key request\ntype DeleteAPIKeyReq struct {\n\tID     int    `json:\"id\"`\n\tUserID string `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/schema/backyard_user_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// UpdateUserStatusReq update user request\ntype UpdateUserStatusReq struct {\n\tUserID           string `validate:\"required\" json:\"user_id\"`\n\tStatus           string `validate:\"required,oneof=normal suspended deleted inactive\" json:\"status\" enums:\"normal,suspended,deleted,inactive\"`\n\tSuspendDuration  string `validate:\"omitempty,oneof=24h 48h 72h 7d 14d 1m 2m 3m 6m 1y forever\" json:\"suspend_duration\"`\n\tRemoveAllContent bool   `validate:\"omitempty\" json:\"remove_all_content\"`\n\tLoginUserID      string `json:\"-\"`\n}\n\nfunc (r *UpdateUserStatusReq) IsNormal() bool    { return r.Status == constant.UserNormal }\nfunc (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == constant.UserSuspended }\nfunc (r *UpdateUserStatusReq) IsDeleted() bool   { return r.Status == constant.UserDeleted }\nfunc (r *UpdateUserStatusReq) IsInactive() bool  { return r.Status == constant.UserInactive }\n\n// GetSuspendedUntil calculates the suspended until time based on duration\nfunc (r *UpdateUserStatusReq) GetSuspendedUntil() time.Time {\n\tif !r.IsSuspended() || r.SuspendDuration == \"\" || r.SuspendDuration == \"forever\" {\n\t\treturn entity.PermanentSuspensionTime // permanent suspension\n\t}\n\n\tnow := time.Now()\n\tswitch r.SuspendDuration {\n\tcase \"24h\":\n\t\treturn now.Add(24 * time.Hour)\n\tcase \"48h\":\n\t\treturn now.Add(48 * time.Hour)\n\tcase \"72h\":\n\t\treturn now.Add(72 * time.Hour)\n\tcase \"7d\":\n\t\treturn now.Add(7 * 24 * time.Hour)\n\tcase \"14d\":\n\t\treturn now.Add(14 * 24 * time.Hour)\n\tcase \"1m\":\n\t\treturn now.AddDate(0, 1, 0)\n\tcase \"2m\":\n\t\treturn now.AddDate(0, 2, 0)\n\tcase \"3m\":\n\t\treturn now.AddDate(0, 3, 0)\n\tcase \"6m\":\n\t\treturn now.AddDate(0, 6, 0)\n\tcase \"1y\":\n\t\treturn now.AddDate(1, 0, 0)\n\tdefault:\n\t\treturn entity.PermanentSuspensionTime // fallback to permanent\n\t}\n}\n\n// GetUserPageReq get user list page request\ntype GetUserPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// email\n\tQuery string `validate:\"omitempty,gt=0,lte=100\" form:\"query\"`\n\t// user status\n\tStatus string `validate:\"omitempty,oneof=normal suspended deleted inactive\" form:\"status\"`\n\t// staff, if staff is true means query admin or moderator\n\tStaff bool `validate:\"omitempty\" form:\"staff\"`\n}\n\nfunc (r *GetUserPageReq) IsSuspended() bool { return r.Status == constant.UserSuspended }\nfunc (r *GetUserPageReq) IsDeleted() bool   { return r.Status == constant.UserDeleted }\nfunc (r *GetUserPageReq) IsInactive() bool  { return r.Status == constant.UserInactive }\n\n// GetUserPageResp get user response\ntype GetUserPageResp struct {\n\t// user id\n\tUserID string `json:\"user_id\"`\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// delete time\n\tDeletedAt int64 `json:\"deleted_at\"`\n\t// suspended time\n\tSuspendedAt int64 `json:\"suspended_at\"`\n\t// suspended until time\n\tSuspendedUntil int64 `json:\"suspended_until\"`\n\t// username\n\tUsername string `json:\"username\"`\n\t// email\n\tEMail string `json:\"e_mail\"`\n\t// rank\n\tRank int `json:\"rank\"`\n\t// user status(normal,suspended,deleted,inactive)\n\tStatus string `json:\"status\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// avatar\n\tAvatar string `json:\"avatar\"`\n\t// role id\n\tRoleID int `json:\"role_id\"`\n\t// role name\n\tRoleName string `json:\"role_name\"`\n}\n\n// GetUserInfoReq get user request\ntype GetUserInfoReq struct {\n\tUserID string `validate:\"required\" json:\"user_id\"`\n}\n\n// GetUserInfoResp get user response\ntype GetUserInfoResp struct {\n\t// suspended until\n\tSuspendedUntil time.Time `json:\"suspended_until\"`\n}\n\n// UpdateUserRoleReq update user role request\ntype UpdateUserRoleReq struct {\n\t// user id\n\tUserID string `validate:\"required\" json:\"user_id\"`\n\t// role id\n\tRoleID int `validate:\"required\" json:\"role_id\"`\n\t// login user id\n\tLoginUserID string `json:\"-\"`\n}\n\n// EditUserProfileReq edit user profile request\ntype EditUserProfileReq struct {\n\tUserID      string `validate:\"required\" json:\"user_id\"`\n\tDisplayName string `validate:\"required,gte=2,lte=30\" json:\"display_name\"`\n\tUsername    string `validate:\"omitempty,gte=2,lte=30\" json:\"username\"`\n\tEmail       string `validate:\"required,email,gt=0,lte=500\" json:\"email\"`\n\tLoginUserID string `json:\"-\"`\n\tIsAdmin     bool   `json:\"-\"`\n}\n\n// AddUserReq add user request\ntype AddUserReq struct {\n\tDisplayName string `validate:\"required,gte=2,lte=30\" json:\"display_name\"`\n\tEmail       string `validate:\"required,email,gt=0,lte=500\" json:\"email\"`\n\tPassword    string `validate:\"required,gte=8,lte=32\" json:\"password\"`\n\tLoginUserID string `json:\"-\"`\n}\n\n// AddUsersReq add users request\ntype AddUsersReq struct {\n\t// users info line by line\n\tUsersStr string        `json:\"users\"`\n\tUsers    []*AddUserReq `json:\"-\"`\n}\n\n// DeletePermanentlyReq delete permanently request\ntype DeletePermanentlyReq struct {\n\tType string `validate:\"required,oneof=users questions answers\" json:\"type\"`\n}\n\ntype AddUsersErrorData struct {\n\t// optional. error field name.\n\tField string `json:\"field\"`\n\t// must. error line number.\n\tLine int `json:\"line\"`\n\t// must. error content.\n\tContent string `json:\"content\"`\n\t// optional. error message.\n\tExtraMessage string `json:\"extra_message\"`\n}\n\nfunc (e *AddUsersErrorData) GetErrField(ctx context.Context) (errFields []*validator.FormErrorField) {\n\treturn append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\tErrorField: \"users\",\n\t\tErrorMsg:   translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError, e),\n\t})\n}\n\nfunc (req *AddUsersReq) ParseUsers(ctx context.Context) (errFields []*validator.FormErrorField, err error) {\n\treq.UsersStr = strings.TrimSpace(req.UsersStr)\n\tlines := strings.Split(req.UsersStr, \"\\n\")\n\treq.Users = make([]*AddUserReq, 0)\n\tfor i, line := range lines {\n\t\tarr := strings.Split(line, \",\")\n\t\tif len(arr) != 3 {\n\t\t\terrFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\t\tErrorField: \"users\",\n\t\t\t\tErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError,\n\t\t\t\t\t&AddUsersErrorData{\n\t\t\t\t\t\tLine:    i + 1,\n\t\t\t\t\t\tContent: line,\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\treturn errFields, errors.BadRequest(reason.RequestFormatError)\n\t\t}\n\t\treq.Users = append(req.Users, &AddUserReq{\n\t\t\tDisplayName: strings.TrimSpace(arr[0]),\n\t\t\tEmail:       strings.TrimSpace(arr[1]),\n\t\t\tPassword:    strings.TrimSpace(arr[2]),\n\t\t})\n\t}\n\n\t// check users amount\n\tif len(req.Users) == 0 || len(req.Users) > constant.DefaultBulkUser {\n\t\terrFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\tErrorField: \"users\",\n\t\t\tErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersAmountError,\n\t\t\t\tmap[string]int{\n\t\t\t\t\t\"MaxAmount\": constant.DefaultBulkUser,\n\t\t\t\t}),\n\t\t})\n\t\treturn errFields, errors.BadRequest(reason.RequestFormatError)\n\t}\n\treturn nil, nil\n}\n\n// UpdateUserPasswordReq update user password request\ntype UpdateUserPasswordReq struct {\n\tUserID      string `validate:\"required\" json:\"user_id\"`\n\tPassword    string `validate:\"required,gte=8,lte=32\" json:\"password\"`\n\tLoginUserID string `json:\"-\"`\n}\n\n// GetUserActivationReq get user activation\ntype GetUserActivationReq struct {\n\tUserID string `validate:\"required\" form:\"user_id\"`\n}\n\n// GetUserActivationResp get user activation\ntype GetUserActivationResp struct {\n\tActivationURL string `json:\"activation_url\"`\n}\n\n// SendUserActivationReq send user activation\ntype SendUserActivationReq struct {\n\tUserID string `validate:\"required\" json:\"user_id\"`\n}\n"
  },
  {
    "path": "internal/schema/badge_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"github.com/apache/answer/internal/entity\"\n\nconst (\n\tBadgeStatusActive   BadgeStatus = \"active\"\n\tBadgeStatusInactive BadgeStatus = \"inactive\"\n)\n\ntype BadgeStatus string\n\nvar BadgeStatusMap = map[int8]BadgeStatus{\n\tentity.BadgeStatusActive:   BadgeStatusActive,\n\tentity.BadgeStatusInactive: BadgeStatusInactive,\n}\n\nvar BadgeStatusEMap = map[BadgeStatus]int8{\n\tBadgeStatusActive:   entity.BadgeStatusActive,\n\tBadgeStatusInactive: entity.BadgeStatusInactive,\n}\n\n// BadgeListInfo get badge list response\ntype BadgeListInfo struct {\n\t// badge id\n\tID string `json:\"id\" `\n\t// badge name\n\tName string `json:\"name\" `\n\t// badge icon\n\tIcon string `json:\"icon\" `\n\t// badge award count\n\tAwardCount int `json:\"award_count\" `\n\t// badge earned count\n\tEarnedCount int64 `json:\"earned_count\" `\n\t// badge level\n\tLevel entity.BadgeLevel `json:\"level\" `\n}\n\ntype GetBadgeListResp struct {\n\t// badge list info\n\tBadges []*BadgeListInfo `json:\"badges\" `\n\t// badge group name\n\tGroupName string `json:\"group_name\" `\n}\n\ntype UpdateBadgeStatusReq struct {\n\t// badge id\n\tID string `validate:\"required\" json:\"id\"`\n\t// badge status\n\tStatus BadgeStatus `validate:\"required\" json:\"status\"`\n}\n\ntype GetBadgeListPagedReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// badge status\n\tStatus BadgeStatus `validate:\"omitempty\" form:\"status\"`\n\t// query condition\n\tQuery string `validate:\"omitempty\" form:\"q\"`\n}\n\ntype GetBadgeListPagedResp struct {\n\t// badge id\n\tID string `json:\"id\" `\n\t// badge name\n\tName string `json:\"name\" `\n\t// badge description\n\tDescription string `json:\"description\" `\n\t// badge icon\n\tIcon string `json:\"icon\" `\n\t// badge award count\n\tAwardCount int `json:\"award_count\" `\n\t// badge earned count\n\tEarned bool `json:\"earned\" `\n\t// badge level\n\tLevel entity.BadgeLevel `json:\"level\" `\n\t// badge group name\n\tGroupName string `json:\"group_name\" `\n\t// badge status\n\tStatus BadgeStatus `json:\"status\"`\n}\n\ntype GetBadgeInfoResp struct {\n\t// badge id\n\tID string `json:\"id\" `\n\t// badge name\n\tName string `json:\"name\" `\n\t// badge description\n\tDescription string `json:\"description\" `\n\t// badge icon\n\tIcon string `json:\"icon\" `\n\t// badge award count\n\tAwardCount int `json:\"award_count\" `\n\t// badge earned count\n\tEarnedCount int64 `json:\"earned_count\" `\n\t// badge is single or multiple\n\tIsSingle bool `json:\"is_single\" `\n\t// badge level\n\tLevel entity.BadgeLevel `json:\"level\" `\n}\n\ntype GetBadgeAwardWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// badge id\n\tBadgeID string `validate:\"required\" form:\"badge_id\"`\n\t// username\n\tUsername string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\ntype GetBadgeAwardWithPageResp struct {\n\t// created time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// object id\n\tObjectID string `json:\"object_id\"`\n\t// question id\n\tQuestionID string `json:\"question_id\"`\n\t// answer id\n\tAnswerID string `json:\"answer_id\"`\n\t// comment id\n\tCommentID string `json:\"comment_id\"`\n\t// object type\n\tObjectType string `json:\"object_type\" enums:\"question,answer,comment\"`\n\t// url title\n\tUrlTitle string `json:\"url_title\"`\n\t// author user info\n\tAuthorUserInfo UserBasicInfo `json:\"author_user_info\"`\n}\n\ntype GetUserBadgeAwardListReq struct {\n\t// username\n\tUsername string `validate:\"required,gt=0,lte=100\" form:\"username\"`\n\t// user id\n\tUserID string `json:\"-\"`\n\tLimit  int    `json:\"-\"`\n}\n\ntype GetUserBadgeAwardListResp struct {\n\t// badge id\n\tID string `json:\"id\" `\n\t// badge name\n\tName string `json:\"name\" `\n\t// badge icon\n\tIcon string `json:\"icon\" `\n\t// badge award count\n\tEarnedCount int64 `json:\"earned_count\" `\n\t// badge level\n\tLevel entity.BadgeLevel `json:\"level\" `\n}\n\ntype BadgeTplData struct {\n\tProfileURL string\n}\n"
  },
  {
    "path": "internal/schema/collection_group_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"time\"\n\nconst (\n\tCGDefault = 1\n\tCGDIY     = 2\n)\n\n// CollectionSwitchReq switch collection request\ntype CollectionSwitchReq struct {\n\tObjectID string `validate:\"required\" json:\"object_id\"`\n\tGroupID  string `validate:\"required\" json:\"group_id\"`\n\tBookmark bool   `validate:\"omitempty\" json:\"bookmark\"`\n\tUserID   string `json:\"-\"`\n}\n\n// CollectionSwitchResp switch collection response\ntype CollectionSwitchResp struct {\n\tObjectCollectionCount int64 `json:\"object_collection_count\"`\n}\n\n// AddCollectionGroupReq add collection group request\ntype AddCollectionGroupReq struct {\n\t//\n\tUserID int64 `validate:\"required\" comment:\"\" json:\"user_id\"`\n\t// the collection group name\n\tName string `validate:\"required,gt=0,lte=50\" comment:\"the collection group name\" json:\"name\"`\n\t// mark this group is default, default 1\n\tDefaultGroup int `validate:\"required\" comment:\"mark this group is default, default 1\" json:\"default_group\"`\n\t//\n\tCreateTime time.Time `validate:\"required\" comment:\"\" json:\"create_time\"`\n\t//\n\tUpdateTime time.Time `validate:\"required\" comment:\"\" json:\"update_time\"`\n}\n\n// UpdateCollectionGroupReq update collection group request\ntype UpdateCollectionGroupReq struct {\n\t//\n\tID int64 `validate:\"required\" comment:\"\" json:\"id\"`\n\t//\n\tUserID int64 `validate:\"omitempty\" comment:\"\" json:\"user_id\"`\n\t// the collection group name\n\tName string `validate:\"omitempty,gt=0,lte=50\" comment:\"the collection group name\" json:\"name\"`\n\t// mark this group is default, default 1\n\tDefaultGroup int `validate:\"omitempty\" comment:\"mark this group is default, default 1\" json:\"default_group\"`\n\t//\n\tCreateTime time.Time `validate:\"omitempty\" comment:\"\" json:\"create_time\"`\n\t//\n\tUpdateTime time.Time `validate:\"omitempty\" comment:\"\" json:\"update_time\"`\n}\n\n// GetCollectionGroupResp get collection group response\ntype GetCollectionGroupResp struct {\n\t//\n\tID int64 `json:\"id\"`\n\t//\n\tUserID int64 `json:\"user_id\"`\n\t// the collection group name\n\tName string `json:\"name\"`\n\t// mark this group is default, default 1\n\tDefaultGroup int `json:\"default_group\"`\n\t//\n\tCreateTime time.Time `json:\"create_time\"`\n\t//\n\tUpdateTime time.Time `json:\"update_time\"`\n}\n"
  },
  {
    "path": "internal/schema/comment_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// AddCommentReq add comment request\ntype AddCommentReq struct {\n\t// object id\n\tObjectID string `validate:\"required\" json:\"object_id\"`\n\t// reply comment id\n\tReplyCommentID string `validate:\"omitempty\" json:\"reply_comment_id\"`\n\t// original comment content\n\tOriginalText string `validate:\"required,notblank,gte=2,lte=600\" json:\"original_text\"`\n\t// parsed comment content\n\tParsedText string `json:\"-\"`\n\t// @ user id list\n\tMentionUsernameList []string `validate:\"omitempty\" json:\"mention_username_list\"`\n\tCaptchaID           string   `json:\"captcha_id\"`\n\tCaptchaCode         string   `json:\"captcha_code\"`\n\n\t// user id\n\tUserID string `json:\"-\"`\n\t// whether user can add it\n\tCanAdd bool `json:\"-\"`\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n\t// whether user can delete it\n\tCanDelete bool `json:\"-\"`\n\n\tIP        string `json:\"-\"`\n\tUserAgent string `json:\"-\"`\n}\n\nfunc (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.ParsedText = converter.Markdown2HTML(req.OriginalText)\n\tif req.ParsedText == \"\" {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"original_text\",\n\t\t\tErrorMsg:   reason.CommentContentCannotEmpty,\n\t\t}), errors.BadRequest(reason.CommentContentCannotEmpty)\n\t}\n\treturn nil, nil\n}\n\n// RemoveCommentReq remove comment\ntype RemoveCommentReq struct {\n\t// comment id\n\tCommentID string `validate:\"required\" json:\"comment_id\"`\n\t// user id\n\tUserID      string `json:\"-\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\n// UpdateCommentReq update comment request\ntype UpdateCommentReq struct {\n\t// comment id\n\tCommentID string `validate:\"required\" json:\"comment_id\"`\n\t// original comment content\n\tOriginalText string `validate:\"required,notblank,gte=2,lte=600\" json:\"original_text\"`\n\t// parsed comment content\n\tParsedText string `json:\"-\"`\n\t// user id\n\tUserID  string `json:\"-\"`\n\tIsAdmin bool   `json:\"-\"`\n\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n\n\t// whether user can delete it\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\nfunc (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.ParsedText = converter.Markdown2HTML(req.OriginalText)\n\tif req.ParsedText == \"\" {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"original_text\",\n\t\t\tErrorMsg:   reason.CommentContentCannotEmpty,\n\t\t}), errors.BadRequest(reason.CommentContentCannotEmpty)\n\t}\n\treturn nil, nil\n}\n\ntype UpdateCommentResp struct {\n\t// comment id\n\tCommentID string `json:\"comment_id\"`\n\t// original comment content\n\tOriginalText string `json:\"original_text\"`\n\t// parsed comment content\n\tParsedText string `json:\"parsed_text\"`\n}\n\n// GetCommentListReq get comment list all request\ntype GetCommentListReq struct {\n\t// user id\n\tUserID int64 `validate:\"omitempty\" comment:\"user id\" form:\"user_id\"`\n\t// reply user id\n\tReplyUserID int64 `validate:\"omitempty\" comment:\"reply user id\" form:\"reply_user_id\"`\n\t// reply comment id\n\tReplyCommentID int64 `validate:\"omitempty\" comment:\"reply comment id\" form:\"reply_comment_id\"`\n\t// object id\n\tObjectID int64 `validate:\"omitempty\" comment:\"object id\" form:\"object_id\"`\n\t// user vote amount\n\tVoteCount int `validate:\"omitempty\" comment:\"user vote amount\" form:\"vote_count\"`\n\t// comment status(available: 0; deleted: 10)\n\tStatus int `validate:\"omitempty\" comment:\"comment status(available: 0; deleted: 10)\" form:\"status\"`\n\t// original comment content\n\tOriginalText string `validate:\"omitempty\" comment:\"original comment content\" form:\"original_text\"`\n\t// parsed comment content\n\tParsedText string `validate:\"omitempty\" comment:\"parsed comment content\" form:\"parsed_text\"`\n}\n\n// GetCommentWithPageReq get comment list page request\ntype GetCommentWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// object id\n\tObjectID string `validate:\"required\" form:\"object_id\"`\n\t// comment id\n\tCommentID string `validate:\"omitempty\" form:\"comment_id\"`\n\t// query condition\n\tQueryCond string `validate:\"omitempty,oneof=vote created_at\" form:\"query_cond\"`\n\t// user id\n\tUserID string `json:\"-\"`\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n\t// whether user can delete it\n\tCanDelete bool `json:\"-\"`\n}\n\n// GetCommentReq get comment list page request\ntype GetCommentReq struct {\n\t// object id\n\tID string `validate:\"required\" form:\"id\"`\n\t// user id\n\tUserID string `json:\"-\"`\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n\t// whether user can delete it\n\tCanDelete bool `json:\"-\"`\n}\n\n// GetCommentResp comment response\ntype GetCommentResp struct {\n\t// comment id\n\tCommentID string `json:\"comment_id\"`\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\n\t// object id\n\tObjectID string `json:\"object_id\"`\n\t// user vote amount\n\tVoteCount int `json:\"vote_count\"`\n\t// current user if already vote this comment\n\tIsVote bool `json:\"is_vote\"`\n\t// original comment content\n\tOriginalText string `json:\"original_text\"`\n\t// parsed comment content\n\tParsedText string `json:\"parsed_text\"`\n\n\t// user id\n\tUserID string `json:\"user_id\"`\n\t// username\n\tUsername string `json:\"username\"`\n\t// user display name\n\tUserDisplayName string `json:\"user_display_name\"`\n\t// user avatar\n\tUserAvatar string `json:\"user_avatar\"`\n\t// user status\n\tUserStatus string `json:\"user_status\"`\n\n\t// reply user id\n\tReplyUserID string `json:\"reply_user_id\"`\n\t// reply user username\n\tReplyUsername string `json:\"reply_username\"`\n\t// reply user display name\n\tReplyUserDisplayName string `json:\"reply_user_display_name\"`\n\t// reply comment id\n\tReplyCommentID string `json:\"reply_comment_id\"`\n\t// reply user status\n\tReplyUserStatus string `json:\"reply_user_status\"`\n\n\t// MemberActions\n\tMemberActions []*PermissionMemberAction `json:\"member_actions\"`\n}\n\nfunc (r *GetCommentResp) SetFromComment(comment *entity.Comment) {\n\t_ = copier.Copy(r, comment)\n\tr.CommentID = comment.ID\n\tr.CreatedAt = comment.CreatedAt.Unix()\n\tr.ReplyUserID = comment.GetReplyUserID()\n\tr.ReplyCommentID = comment.GetReplyCommentID()\n}\n\n// GetCommentPersonalWithPageReq get comment list page request\ntype GetCommentPersonalWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// username\n\tUsername string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\n// GetCommentPersonalWithPageResp comment response\ntype GetCommentPersonalWithPageResp struct {\n\t// comment id\n\tCommentID string `json:\"comment_id\"`\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// object id\n\tObjectID string `json:\"object_id\"`\n\t// question id\n\tQuestionID string `json:\"question_id\"`\n\t// answer id\n\tAnswerID string `json:\"answer_id\"`\n\t// object type\n\tObjectType string `json:\"object_type\" enums:\"question,answer,tag,comment\"`\n\t// title\n\tTitle string `json:\"title\"`\n\t// url title\n\tUrlTitle string `json:\"url_title\"`\n\t// content\n\tContent string `json:\"content\"`\n}\n"
  },
  {
    "path": "internal/schema/config_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// AddConfigReq add config request\ntype AddConfigReq struct {\n\t// the config key\n\tKey string `validate:\"omitempty,gt=0,lte=32\" comment:\"the config key\" json:\"key\"`\n\t// the config value, custom data structures and types\n\tValue string `validate:\"omitempty,gt=0,lte=128\" comment:\"the config value, custom data structures and types\" json:\"value\"`\n}\n\n// RemoveConfigReq delete config request\ntype RemoveConfigReq struct {\n\t// config id\n\tID int `validate:\"required\" comment:\"config id\" json:\"id\"`\n}\n\n// UpdateConfigReq update config request\ntype UpdateConfigReq struct {\n\t// config id\n\tID int `validate:\"required\" comment:\"config id\" json:\"id\"`\n\t// the config key\n\tKey string `validate:\"omitempty,gt=0,lte=32\" comment:\"the config key\" json:\"key\"`\n\t// the config value, custom data structures and types\n\tValue string `validate:\"omitempty,gt=0,lte=128\" comment:\"the config value, custom data structures and types\" json:\"value\"`\n}\n\n// GetConfigListReq get config list all request\ntype GetConfigListReq struct {\n\t// the config key\n\tKey string `validate:\"omitempty,gt=0,lte=32\" comment:\"the config key\" form:\"key\"`\n\t// the config value, custom data structures and types\n\tValue string `validate:\"omitempty,gt=0,lte=128\" comment:\"the config value, custom data structures and types\" form:\"value\"`\n}\n\n// GetConfigWithPageReq get config list page request\ntype GetConfigWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// the config key\n\tKey string `validate:\"omitempty,gt=0,lte=32\" comment:\"the config key\" form:\"key\"`\n\t// the config value, custom data structures and types\n\tValue string `validate:\"omitempty,gt=0,lte=128\" comment:\"the config value, custom data structures and types\" form:\"value\"`\n}\n\n// GetConfigResp get config response\ntype GetConfigResp struct {\n\t// config id\n\tID int `json:\"id\"`\n\t// the config key\n\tKey string `json:\"key\"`\n\t// the config value, custom data structures and types\n\tValue string `json:\"value\"`\n}\n"
  },
  {
    "path": "internal/schema/connector_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\ntype ConnectorInfoResp struct {\n\tName string `json:\"name\"`\n\tIcon string `json:\"icon\"`\n\tLink string `json:\"link\"`\n}\n\ntype ConnectorUserInfoResp struct {\n\tName       string `json:\"name\"`\n\tIcon       string `json:\"icon\"`\n\tLink       string `json:\"link\"`\n\tBinding    bool   `json:\"binding\"`\n\tExternalID string `json:\"external_id\"`\n}\n"
  },
  {
    "path": "internal/schema/dashboard_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"time\"\n\nvar AppStartTime time.Time\n\nconst (\n\tDashboardCacheKey  = \"answer:dashboard\"\n\tDashboardCacheTime = 60 * time.Minute\n)\n\ntype DashboardInfo struct {\n\tQuestionCount         int64                `json:\"question_count\"`\n\tResolvedCount         int64                `json:\"resolved_count\"`\n\tResolvedRate          string               `json:\"resolved_rate\"`\n\tUnansweredCount       int64                `json:\"unanswered_count\"`\n\tUnansweredRate        string               `json:\"unanswered_rate\"`\n\tAnswerCount           int64                `json:\"answer_count\"`\n\tCommentCount          int64                `json:\"comment_count\"`\n\tVoteCount             int64                `json:\"vote_count\"`\n\tUserCount             int64                `json:\"user_count\"`\n\tReportCount           int64                `json:\"report_count\"`\n\tUploadingFiles        bool                 `json:\"uploading_files\"`\n\tSMTP                  string               `json:\"smtp\"`\n\tHTTPS                 bool                 `json:\"https\"`\n\tTimeZone              string               `json:\"time_zone\"`\n\tOccupyingStorageSpace string               `json:\"occupying_storage_space\"`\n\tAppStartTime          string               `json:\"app_start_time\"`\n\tVersionInfo           DashboardInfoVersion `json:\"version_info\"`\n\tLoginRequired         bool                 `json:\"login_required\"`\n\tGoVersion             string               `json:\"go_version\"`\n\tDatabaseVersion       string               `json:\"database_version\"`\n\tDatabaseSize          string               `json:\"database_size\"`\n}\n\ntype DashboardInfoVersion struct {\n\tVersion       string `json:\"version\"`\n\tRevision      string `json:\"revision\"`\n\tRemoteVersion string `json:\"remote_version\"`\n}\n\ntype RemoteVersion struct {\n\tRelease struct {\n\t\tVersion string `json:\"version\"`\n\t\tURL     string `json:\"url\"`\n\t} `json:\"release\"`\n}\n"
  },
  {
    "path": "internal/schema/email_template.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n)\n\nconst (\n\tAccountActivationSourceType EmailSourceType = \"account-activation\"\n\tPasswordResetSourceType     EmailSourceType = \"password-reset\"\n\tConfirmNewEmailSourceType   EmailSourceType = \"password-reset\"\n\tUnsubscribeSourceType       EmailSourceType = \"unsubscribe\"\n\tBindingSourceType           EmailSourceType = \"binding\"\n)\n\ntype EmailSourceType string\n\ntype EmailCodeContent struct {\n\tSourceType EmailSourceType `json:\"source_type\"`\n\tEmail      string          `json:\"e_mail\"`\n\tUserID     string          `json:\"user_id\"`\n\t// Used for unsubscribe notification\n\tNotificationSources []constant.NotificationSource `json:\"notification_source,omitempty\"`\n\t// Used for third-party login account binding\n\tBindingKey string `json:\"binding_key,omitempty\"`\n\t// Skip the validation of the latest code\n\tSkipValidationLatestCode bool `json:\"skip_validation_latest_code\"`\n}\n\nfunc (r *EmailCodeContent) ToJSONString() string {\n\tcodeBytes, _ := json.Marshal(r)\n\treturn string(codeBytes)\n}\n\nfunc (r *EmailCodeContent) FromJSONString(data string) error {\n\treturn json.Unmarshal([]byte(data), &r)\n}\n\ntype RegisterTemplateData struct {\n\tSiteName    string\n\tRegisterUrl string\n}\n\ntype PassResetTemplateData struct {\n\tSiteName     string\n\tPassResetUrl string\n}\n\ntype ChangeEmailTemplateData struct {\n\tSiteName       string\n\tChangeEmailUrl string\n}\n\ntype TestTemplateData struct {\n\tSiteName string\n}\n\ntype NewAnswerTemplateRawData struct {\n\tAnswerUserDisplayName string\n\tQuestionTitle         string\n\tQuestionID            string\n\tAnswerID              string\n\tAnswerSummary         string\n\tUnsubscribeCode       string\n}\n\ntype NewAnswerTemplateData struct {\n\tSiteName       string\n\tDisplayName    string\n\tQuestionTitle  string\n\tAnswerUrl      string\n\tAnswerSummary  string\n\tUnsubscribeUrl string\n}\n\ntype NewInviteAnswerTemplateRawData struct {\n\tInviterDisplayName string\n\tQuestionTitle      string\n\tQuestionID         string\n\tUnsubscribeCode    string\n}\n\ntype NewInviteAnswerTemplateData struct {\n\tSiteName       string\n\tDisplayName    string\n\tQuestionTitle  string\n\tInviteUrl      string\n\tUnsubscribeUrl string\n}\n\ntype NewCommentTemplateRawData struct {\n\tCommentUserDisplayName string\n\tQuestionTitle          string\n\tQuestionID             string\n\tAnswerID               string\n\tCommentID              string\n\tCommentSummary         string\n\tUnsubscribeCode        string\n}\n\ntype NewCommentTemplateData struct {\n\tSiteName       string\n\tDisplayName    string\n\tQuestionTitle  string\n\tCommentUrl     string\n\tCommentSummary string\n\tUnsubscribeUrl string\n}\n\ntype NewQuestionTemplateRawData struct {\n\tQuestionAuthorUserID string\n\tQuestionTitle        string\n\tQuestionID           string\n\tUnsubscribeCode      string\n\tTags                 []string\n\tTagIDs               []string\n}\n\ntype NewQuestionTemplateData struct {\n\tSiteName       string\n\tQuestionTitle  string\n\tQuestionUrl    string\n\tTags           string\n\tUnsubscribeUrl string\n}\n"
  },
  {
    "path": "internal/schema/err_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\ntype ErrTypeData struct {\n\tErrType string `json:\"err_type\"`\n}\n\nvar ErrTypeModal = ErrTypeData{ErrType: \"modal\"}\n\nvar ErrTypeToast = ErrTypeData{ErrType: \"toast\"}\n\nvar ErrTypeAlert = ErrTypeData{ErrType: \"alert\"}\n"
  },
  {
    "path": "internal/schema/event_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\n// EventMsg event message\ntype EventMsg struct {\n\tEventType constant.EventType\n\tUserID    string\n\n\tTriggerObjectID string\n\n\tQuestionID     string\n\tQuestionUserID string\n\n\tAnswerID     string\n\tAnswerUserID string\n\n\tCommentID     string\n\tCommentUserID string\n\n\tExtraInfo map[string]string\n}\n\n// NewEvent create a new event\nfunc NewEvent(e constant.EventType, userID string) *EventMsg {\n\treturn &EventMsg{\n\t\tUserID:    userID,\n\t\tEventType: e,\n\t\tExtraInfo: make(map[string]string),\n\t}\n}\n\n// QID get question id\nfunc (e *EventMsg) QID(questionID, userID string) *EventMsg {\n\tif len(questionID) > 0 {\n\t\te.QuestionID = uid.DeShortID(questionID)\n\t}\n\te.QuestionUserID = userID\n\treturn e\n}\n\n// AID get answer id\nfunc (e *EventMsg) AID(answerID, userID string) *EventMsg {\n\tif len(answerID) > 0 {\n\t\te.AnswerID = uid.DeShortID(answerID)\n\t}\n\te.AnswerUserID = userID\n\treturn e\n}\n\n// CID get comment id\nfunc (e *EventMsg) CID(comment, userID string) *EventMsg {\n\te.CommentID = comment\n\te.CommentUserID = userID\n\treturn e\n}\n\n// TID get trigger object id\nfunc (e *EventMsg) TID(triggerObjectID string) *EventMsg {\n\tif len(triggerObjectID) > 0 {\n\t\te.TriggerObjectID = uid.DeShortID(triggerObjectID)\n\t}\n\treturn e\n}\n\n// AddExtra add extra info\nfunc (e *EventMsg) AddExtra(key, value string) *EventMsg {\n\te.ExtraInfo[key] = value\n\treturn e\n}\n\n// GetExtra get extra info\nfunc (e *EventMsg) GetExtra(key string) string {\n\tif v, ok := e.ExtraInfo[key]; ok {\n\t\treturn v\n\t}\n\treturn \"\"\n}\n\n// GetObjectID get object id\nfunc (e *EventMsg) GetObjectID() string {\n\tif len(e.TriggerObjectID) > 0 {\n\t\treturn e.TriggerObjectID\n\t}\n\tif len(e.CommentID) > 0 {\n\t\treturn e.CommentID\n\t}\n\tif len(e.AnswerID) > 0 {\n\t\treturn e.AnswerID\n\t}\n\treturn e.QuestionID\n}\n"
  },
  {
    "path": "internal/schema/follow_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// FollowReq follow object request\ntype FollowReq struct {\n\t// object id\n\tObjectID string `validate:\"required\" form:\"object_id\" json:\"object_id\"`\n\t// is cancel\n\tIsCancel bool `validate:\"omitempty\" form:\"is_cancel\" json:\"is_cancel\"`\n}\n\n// FollowResp response object's follows and current user follow status\ntype FollowResp struct {\n\t// the followers of object\n\tFollows int `json:\"follows\"`\n\t// if user is followed object will be true,otherwise false\n\tIsFollowed bool `json:\"is_followed\"`\n}\n\ntype FollowDTO struct {\n\t// object TagID\n\tObjectID string\n\t// is cancel\n\tIsCancel bool\n\t// user TagID\n\tUserID string\n}\n\n// UpdateFollowTagsReq update user follow tags\ntype UpdateFollowTagsReq struct {\n\t// tag slug name list\n\tSlugNameList []string `json:\"slug_name_list\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/schema/forbidden_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nconst (\n\tForbiddenReasonTypeInactive      = \"inactive\"\n\tForbiddenReasonTypeURLExpired    = \"url_expired\"\n\tForbiddenReasonTypeUserSuspended = \"suspended\"\n)\n\n// ForbiddenResp forbidden response\ntype ForbiddenResp struct {\n\t// forbidden reason type\n\tType string `json:\"type\" enums:\"inactive,url_expired\"`\n}\n"
  },
  {
    "path": "internal/schema/mcp_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"strings\"\n\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nconst (\n\tMCPSearchCondKeyword    = \"keyword\"\n\tMCPSearchCondUsername   = \"username\"\n\tMCPSearchCondScore      = \"score\"\n\tMCPSearchCondTag        = \"tag\"\n\tMCPSearchCondPage       = \"page\"\n\tMCPSearchCondPageSize   = \"page_size\"\n\tMCPSearchCondTagName    = \"tag_name\"\n\tMCPSearchCondQuestionID = \"question_id\"\n\tMCPSearchCondObjectID   = \"object_id\"\n)\n\ntype MCPSearchCond struct {\n\tKeyword    string   `json:\"keyword\"`\n\tUsername   string   `json:\"username\"`\n\tScore      int      `json:\"score\"`\n\tTags       []string `json:\"tags\"`\n\tQuestionID string   `json:\"question_id\"`\n}\n\ntype MCPSearchQuestionDetail struct {\n\tQuestionID string `json:\"question_id\"`\n}\n\ntype MCPSearchCommentCond struct {\n\tObjectID string `json:\"object_id\"`\n}\n\ntype MCPSearchTagCond struct {\n\tTagName string `json:\"tag_name\"`\n}\n\ntype MCPSearchUserCond struct {\n\tUsername string `json:\"username\"`\n}\n\ntype MCPSearchQuestionInfoResp struct {\n\tQuestionID string `json:\"question_id\"`\n\tTitle      string `json:\"title\"`\n\tContent    string `json:\"content\"`\n\tLink       string `json:\"link\"`\n}\n\ntype MCPSearchAnswerInfoResp struct {\n\tQuestionID    string `json:\"question_id\"`\n\tQuestionTitle string `json:\"question_title,omitempty\"`\n\tAnswerID      string `json:\"answer_id\"`\n\tAnswerContent string `json:\"answer_content\"`\n\tLink          string `json:\"link\"`\n}\n\ntype MCPSearchTagResp struct {\n\tTagName     string `json:\"tag_name\"`\n\tDisplayName string `json:\"display_name\"`\n\tDescription string `json:\"description\"`\n\tLink        string `json:\"link\"`\n}\n\ntype MCPSearchUserInfoResp struct {\n\tUsername    string `json:\"username\"`\n\tDisplayName string `json:\"display_name\"`\n\tAvatar      string `json:\"avatar\"`\n\tLink        string `json:\"link\"`\n}\n\ntype MCPSearchCommentInfoResp struct {\n\tCommentID string `json:\"comment_id\"`\n\tContent   string `json:\"content\"`\n\tObjectID  string `json:\"object_id\"`\n\tLink      string `json:\"link\"`\n}\n\nfunc NewMCPSearchCond(request mcp.CallToolRequest) *MCPSearchCond {\n\tcond := &MCPSearchCond{}\n\tif keyword, ok := getRequestValue(request, MCPSearchCondKeyword); ok {\n\t\tcond.Keyword = keyword\n\t}\n\tif username, ok := getRequestValue(request, MCPSearchCondUsername); ok {\n\t\tcond.Username = username\n\t}\n\tif score, ok := getRequestNumber(request, MCPSearchCondScore); ok {\n\t\tcond.Score = score\n\t}\n\tif tag, ok := getRequestValue(request, MCPSearchCondTag); ok {\n\t\tcond.Tags = strings.Split(tag, \",\")\n\t}\n\tif questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok {\n\t\tcond.QuestionID = questionID\n\t}\n\treturn cond\n}\n\nfunc NewMCPSearchAnswerCond(request mcp.CallToolRequest) *MCPSearchCond {\n\tcond := &MCPSearchCond{}\n\tif questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok {\n\t\tcond.QuestionID = questionID\n\t}\n\treturn cond\n}\n\nfunc NewMCPSearchQuestionDetail(request mcp.CallToolRequest) *MCPSearchQuestionDetail {\n\tcond := &MCPSearchQuestionDetail{}\n\tif questionID, ok := getRequestValue(request, MCPSearchCondQuestionID); ok {\n\t\tcond.QuestionID = questionID\n\t}\n\treturn cond\n}\n\nfunc NewMCPSearchCommentCond(request mcp.CallToolRequest) *MCPSearchCommentCond {\n\tcond := &MCPSearchCommentCond{}\n\tif keyword, ok := getRequestValue(request, MCPSearchCondObjectID); ok {\n\t\tcond.ObjectID = keyword\n\t}\n\treturn cond\n}\n\nfunc NewMCPSearchTagCond(request mcp.CallToolRequest) *MCPSearchTagCond {\n\tcond := &MCPSearchTagCond{}\n\tif tagName, ok := getRequestValue(request, MCPSearchCondTagName); ok {\n\t\tcond.TagName = tagName\n\t}\n\treturn cond\n}\n\nfunc NewMCPSearchUserCond(request mcp.CallToolRequest) *MCPSearchUserCond {\n\tcond := &MCPSearchUserCond{}\n\tif username, ok := getRequestValue(request, MCPSearchCondUsername); ok {\n\t\tcond.Username = username\n\t}\n\treturn cond\n}\n\nfunc getRequestValue(request mcp.CallToolRequest, key string) (string, bool) {\n\tvalue, ok := request.GetArguments()[key].(string)\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\treturn value, true\n}\n\nfunc getRequestNumber(request mcp.CallToolRequest, key string) (int, bool) {\n\tvalue, ok := request.GetArguments()[key].(float64)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\treturn int(value), true\n}\n\nfunc (cond *MCPSearchCond) ToQueryString() string {\n\tvar queryBuilder strings.Builder\n\tif len(cond.Keyword) > 0 {\n\t\tqueryBuilder.WriteString(cond.Keyword)\n\t}\n\tif len(cond.Username) > 0 {\n\t\tqueryBuilder.WriteString(\" user:\" + cond.Username)\n\t}\n\tif cond.Score > 0 {\n\t\tqueryBuilder.WriteString(\" score:\" + converter.IntToString(int64(cond.Score)))\n\t}\n\tif len(cond.Tags) > 0 {\n\t\tfor _, tag := range cond.Tags {\n\t\t\tqueryBuilder.WriteString(\" [\" + tag + \"]\")\n\t\t}\n\t}\n\treturn strings.TrimSpace(queryBuilder.String())\n}\n"
  },
  {
    "path": "internal/schema/mcp_tools/mcp_tools.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage mcp_tools\n\nimport (\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nvar (\n\tMCPToolsList = []mcp.Tool{\n\t\tNewQuestionsTool(),\n\t\tNewAnswersTool(),\n\t\tNewCommentsTool(),\n\t\tNewTagsTool(),\n\t\tNewTagDetailTool(),\n\t\tNewUserTool(),\n\t}\n)\n\nfunc NewQuestionsTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_questions\",\n\t\tmcp.WithDescription(\"Searching for questions that already existed in the system. After the search, you can use the get_answers_by_question_id tool to get answers for the questions.\"),\n\t\tmcp.WithString(schema.MCPSearchCondKeyword,\n\t\t\tmcp.Description(\"Keyword to search for questions. Multiple keywords separated by spaces\"),\n\t\t),\n\t\tmcp.WithString(schema.MCPSearchCondUsername,\n\t\t\tmcp.Description(\"Search for questions that contain only those created by the specified user\"),\n\t\t),\n\t\tmcp.WithString(schema.MCPSearchCondTag,\n\t\t\tmcp.Description(\"Filter by tag (semicolon separated for multiple tags)\"),\n\t\t),\n\t\tmcp.WithString(schema.MCPSearchCondScore,\n\t\t\tmcp.Description(\"Minimum score that the question must have\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n\nfunc NewAnswersTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_answers_by_question_id\",\n\t\tmcp.WithDescription(\"Search for all answers corresponding to the question ID. The question ID is provided by get_questions tool.\"),\n\t\tmcp.WithString(schema.MCPSearchCondQuestionID,\n\t\t\tmcp.Description(\"The ID of the question to which the answer belongs. The question ID is provided by get_questions tool.\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n\nfunc NewCommentsTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_comments\",\n\t\tmcp.WithDescription(\"Searching for comments that already existed in the system\"),\n\t\tmcp.WithString(schema.MCPSearchCondObjectID,\n\t\t\tmcp.Description(\"Queries comments on an object, either a question or an answer. object_id is the id of the object.\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n\nfunc NewTagsTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_tags\",\n\t\tmcp.WithDescription(\"Searching for tags that already existed in the system\"),\n\t\tmcp.WithString(schema.MCPSearchCondTagName,\n\t\t\tmcp.Description(\"Tag name\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n\nfunc NewTagDetailTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_tag_detail\",\n\t\tmcp.WithDescription(\"Get detailed information about a specific tag\"),\n\t\tmcp.WithString(schema.MCPSearchCondTagName,\n\t\t\tmcp.Description(\"Tag name\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n\nfunc NewUserTool() mcp.Tool {\n\tlistFilesTool := mcp.NewTool(\"get_user\",\n\t\tmcp.WithDescription(\"Searching for users that already existed in the system\"),\n\t\tmcp.WithString(schema.MCPSearchCondUsername,\n\t\t\tmcp.Description(\"Username\"),\n\t\t),\n\t)\n\treturn listFilesTool\n}\n"
  },
  {
    "path": "internal/schema/meta_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"slices\"\n\ntype UpdateReactionReq struct {\n\tObjectID string `validate:\"required\" json:\"object_id\"`\n\tEmoji    string `validate:\"required,oneof=heart smile frown\" json:\"emoji\"`\n\tReaction string `validate:\"required,oneof=activate deactivate\" json:\"reaction\"`\n\tUserID   string `json:\"-\"`\n}\n\ntype GetReactionReq struct {\n\tObjectID string `validate:\"required\" form:\"object_id\"`\n\tUserID   string `json:\"-\"`\n}\n\n// ReactionsSummaryMeta reactions summary meta\ntype ReactionsSummaryMeta struct {\n\tReactions []*ReactionSummaryMeta `json:\"reactions\"`\n}\n\n// ReactionSummaryMeta reaction summary meta\ntype ReactionSummaryMeta struct {\n\tEmoji   string   `json:\"emoji\"`\n\tUserIDs []string `json:\"user_ids\"`\n}\n\n// AddReactionSummary add user operation to reaction summary\nfunc (r *ReactionsSummaryMeta) AddReactionSummary(emoji, userID string) {\n\tfor _, reaction := range r.Reactions {\n\t\tif reaction.Emoji != emoji {\n\t\t\tcontinue\n\t\t}\n\t\texist := slices.Contains(reaction.UserIDs, userID)\n\t\tif !exist {\n\t\t\treaction.UserIDs = append(reaction.UserIDs, userID)\n\t\t}\n\t\treturn\n\t}\n\tr.Reactions = append(r.Reactions, &ReactionSummaryMeta{\n\t\tEmoji:   emoji,\n\t\tUserIDs: []string{userID},\n\t})\n}\n\n// RemoveReactionSummary remove user operation from reaction summary\nfunc (r *ReactionsSummaryMeta) RemoveReactionSummary(emoji, userID string) {\n\tupdatedReactions := make([]*ReactionSummaryMeta, 0)\n\tfor _, reaction := range r.Reactions {\n\t\tif reaction.Emoji != emoji && len(reaction.UserIDs) > 0 {\n\t\t\tupdatedReactions = append(updatedReactions, reaction)\n\t\t\tcontinue\n\t\t}\n\t\tupdatedUserIDs := make([]string, 0, len(r.Reactions))\n\t\tfor _, id := range reaction.UserIDs {\n\t\t\tif id != userID {\n\t\t\t\tupdatedUserIDs = append(updatedUserIDs, id)\n\t\t\t}\n\t\t}\n\t\tif len(updatedUserIDs) > 0 {\n\t\t\treaction.UserIDs = updatedUserIDs\n\t\t\tupdatedReactions = append(updatedReactions, reaction)\n\t\t}\n\t}\n\tr.Reactions = updatedReactions\n}\n\n// CheckUserInReactionSummary check user's operation if in reaction summary\nfunc (r *ReactionsSummaryMeta) CheckUserInReactionSummary(emoji, userID string) bool {\n\tfor _, reaction := range r.Reactions {\n\t\tif reaction.Emoji != emoji {\n\t\t\tcontinue\n\t\t}\n\t\tif slices.Contains(reaction.UserIDs, userID) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetReactionByObjectIdResp get reaction by object id response\ntype GetReactionByObjectIdResp struct {\n\tReactionSummary []*ReactionRespItem `json:\"reaction_summary\"`\n}\n\n// ReactionRespItem reaction response item\ntype ReactionRespItem struct {\n\t// Emoji is the reaction emoji\n\tEmoji string `json:\"emoji\"`\n\t// Count is the number of users who reacted\n\tCount int `json:\"count\"`\n\t// Tooltip is the user's name who reacted\n\tTooltip string `json:\"tooltip\"`\n\t// IsActive is if current user has reacted\n\tIsActive bool `json:\"is_active\"`\n}\n"
  },
  {
    "path": "internal/schema/new_question_queue_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\ntype ExternalNotificationMsg struct {\n\tReceiverUserID string `json:\"receiver_user_id\"`\n\tReceiverEmail  string `json:\"receiver_email\"`\n\tReceiverLang   string `json:\"receiver_lang\"`\n\n\tNewAnswerTemplateRawData       *NewAnswerTemplateRawData       `json:\"new_answer_template_raw_data,omitempty\"`\n\tNewInviteAnswerTemplateRawData *NewInviteAnswerTemplateRawData `json:\"new_invite_answer_template_raw_data,omitempty\"`\n\tNewCommentTemplateRawData      *NewCommentTemplateRawData      `json:\"new_comment_template_raw_data,omitempty\"`\n\tNewQuestionTemplateRawData     *NewQuestionTemplateRawData     `json:\"new_question_template_raw_data,omitempty\"`\n}\n\nfunc CreateNewQuestionNotificationMsg(\n\tquestionID, questionTitle, questionAuthorUserID string, tags []*entity.Tag) *ExternalNotificationMsg {\n\tquestionID = uid.DeShortID(questionID)\n\tmsg := &ExternalNotificationMsg{\n\t\tNewQuestionTemplateRawData: &NewQuestionTemplateRawData{\n\t\t\tQuestionAuthorUserID: questionAuthorUserID,\n\t\t\tQuestionID:           questionID,\n\t\t\tQuestionTitle:        questionTitle,\n\t\t},\n\t}\n\tfor _, tag := range tags {\n\t\tmsg.NewQuestionTemplateRawData.Tags = append(msg.NewQuestionTemplateRawData.Tags, tag.SlugName)\n\t\tmsg.NewQuestionTemplateRawData.TagIDs = append(msg.NewQuestionTemplateRawData.TagIDs, tag.ID)\n\t}\n\treturn msg\n}\n"
  },
  {
    "path": "internal/schema/notification_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"encoding/json\"\n\t\"sort\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\nconst (\n\tNotificationTypeInbox        = 1\n\tNotificationTypeAchievement  = 2\n\tNotificationNotRead          = 1\n\tNotificationRead             = 2\n\tNotificationStatusNormal     = 1\n\tNotificationStatusDelete     = 10\n\tNotificationInboxTypeAll     = 0\n\tNotificationInboxTypePosts   = 1\n\tNotificationInboxTypeVotes   = 2\n\tNotificationInboxTypeInvites = 3\n)\n\nvar NotificationType = map[string]int{\n\t\"inbox\":       NotificationTypeInbox,\n\t\"achievement\": NotificationTypeAchievement,\n}\n\nvar NotificationInboxType = map[string]int{\n\t\"all\":     NotificationInboxTypeAll,\n\t\"posts\":   NotificationInboxTypePosts,\n\t\"invites\": NotificationInboxTypeInvites,\n\t\"votes\":   NotificationInboxTypeVotes,\n}\n\ntype NotificationContent struct {\n\tID                 string         `json:\"id\"`\n\tTriggerUserID      string         `json:\"-\"` // show userid\n\tReceiverUserID     string         `json:\"-\"` // receiver userid\n\tUserInfo           *UserBasicInfo `json:\"user_info,omitempty\"`\n\tObjectInfo         ObjectInfo     `json:\"object_info\"`\n\tRank               int            `json:\"rank\"`\n\tNotificationAction string         `json:\"notification_action,omitempty\"`\n\tType               int            `json:\"-\"` //\t1 inbox 2 achievement\n\tIsRead             bool           `json:\"is_read\"`\n\tUpdateTime         int64          `json:\"update_time\"`\n}\n\ntype GetRedDot struct {\n\tCanReviewQuestion bool   `json:\"-\"`\n\tCanReviewAnswer   bool   `json:\"-\"`\n\tCanReviewTag      bool   `json:\"-\"`\n\tUserID            string `json:\"-\"`\n\tIsAdmin           bool   `json:\"-\"`\n}\n\n// NotificationMsg notification message\ntype NotificationMsg struct {\n\t// trigger notification user id\n\tTriggerUserID string\n\t// receive notification user id\n\tReceiverUserID string\n\t// type 1 inbox 2 achievement\n\tType int\n\t// notification title\n\tTitle string\n\t// notification object\n\tObjectID string\n\t// notification object type\n\tObjectType string\n\t// notification action\n\tNotificationAction string\n\t// if true no need to send notification to all followers\n\tNoNeedPushAllFollow bool\n\t// extra info\n\tExtraInfo map[string]string\n}\n\ntype ObjectInfo struct {\n\tTitle      string            `json:\"title\"`\n\tObjectID   string            `json:\"object_id\"`\n\tObjectMap  map[string]string `json:\"object_map\"`\n\tObjectType string            `json:\"object_type\"`\n}\n\ntype RedDot struct {\n\tInbox       int64             `json:\"inbox\"`\n\tAchievement int64             `json:\"achievement\"`\n\tRevision    int64             `json:\"revision\"`\n\tCanRevision bool              `json:\"can_revision\"`\n\tBadgeAward  *RedDotBadgeAward `json:\"badge_award\"`\n}\n\ntype RedDotBadgeAward struct {\n\tNotificationID string            `json:\"notification_id\"`\n\tBadgeID        string            `json:\"badge_id\"`\n\tName           string            `json:\"name\"`\n\tIcon           string            `json:\"icon\"`\n\tLevel          entity.BadgeLevel `json:\"level\"`\n}\n\ntype RedDotBadgeAwardCache struct {\n\tBadgeAwardList map[string]*RedDotBadgeAward `json:\"badge_award_list\"`\n}\n\n// NewRedDotBadgeAwardCache new red dot badge award cache\nfunc NewRedDotBadgeAwardCache() *RedDotBadgeAwardCache {\n\treturn &RedDotBadgeAwardCache{\n\t\tBadgeAwardList: make(map[string]*RedDotBadgeAward),\n\t}\n}\n\n// GetBadgeAward get badge award\nfunc (r *RedDotBadgeAwardCache) GetBadgeAward() *RedDotBadgeAward {\n\tif len(r.BadgeAwardList) == 0 {\n\t\treturn nil\n\t}\n\tvar ids []string\n\tfor _, v := range r.BadgeAwardList {\n\t\tids = append(ids, v.NotificationID)\n\t}\n\tsort.Strings(ids)\n\treturn r.BadgeAwardList[ids[0]]\n}\n\n// FromJSON from json\nfunc (r *RedDotBadgeAwardCache) FromJSON(data string) {\n\t_ = json.Unmarshal([]byte(data), r)\n}\n\n// ToJSON to json\nfunc (r *RedDotBadgeAwardCache) ToJSON() string {\n\tdata, _ := json.Marshal(r)\n\treturn string(data)\n}\n\n// AddBadgeAward add badge award\nfunc (r *RedDotBadgeAwardCache) AddBadgeAward(badgeAward *RedDotBadgeAward) {\n\tif r.BadgeAwardList == nil {\n\t\tr.BadgeAwardList = make(map[string]*RedDotBadgeAward)\n\t}\n\tr.BadgeAwardList[badgeAward.NotificationID] = badgeAward\n}\n\n// RemoveBadgeAward remove badge award\nfunc (r *RedDotBadgeAwardCache) RemoveBadgeAward(notificationID string) {\n\tif r.BadgeAwardList == nil {\n\t\treturn\n\t}\n\tdelete(r.BadgeAwardList, notificationID)\n}\n\ntype NotificationSearch struct {\n\tPage         int    `json:\"page\" form:\"page\"`           // Query number of pages\n\tPageSize     int    `json:\"page_size\" form:\"page_size\"` // Search page size\n\tType         int    `json:\"-\" form:\"-\"`\n\tTypeStr      string `json:\"type\" form:\"type\"`             // inbox achievement\n\tInboxTypeStr string `json:\"inbox_type\" form:\"inbox_type\"` // inbox achievement\n\tInboxType    int    `json:\"-\" form:\"-\"`                   // inbox achievement\n\tUserID       string `json:\"-\"`\n}\n\ntype NotificationClearRequest struct {\n\tNotificationType  string `validate:\"required,oneof=inbox achievement\" json:\"type\"`\n\tUserID            string `json:\"-\"`\n\tCanReviewQuestion bool   `json:\"-\"`\n\tCanReviewAnswer   bool   `json:\"-\"`\n\tCanReviewTag      bool   `json:\"-\"`\n}\n\ntype NotificationClearIDRequest struct {\n\tUserID string `json:\"-\"`\n\tID     string `json:\"id\" form:\"id\"`\n}\n"
  },
  {
    "path": "internal/schema/permission.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\n// PermissionTrTplData template data as for translate permission message\ntype PermissionTrTplData struct {\n\tRank int\n}\n\n// PermissionMemberAction permission member action\ntype PermissionMemberAction struct {\n\tAction string `json:\"action\"`\n\tName   string `json:\"name\"`\n\tType   string `json:\"type\"`\n}\n\n// GetPermissionReq get permission request\ntype GetPermissionReq struct {\n\tAction  string   `form:\"action\"`\n\tActions []string `validate:\"omitempty\" form:\"actions\"`\n}\n\nfunc (r *GetPermissionReq) Check() (errField []*validator.FormErrorField, err error) {\n\tif len(r.Action) > 0 {\n\t\tr.Actions = strings.Split(r.Action, \",\")\n\t}\n\treturn nil, nil\n}\n\n// GetPermissionResp get permission response\ntype GetPermissionResp struct {\n\tHasPermission bool `json:\"has_permission\"`\n\t// only not allow, will return this tip\n\tNoPermissionTip string `json:\"no_permission_tip\"`\n}\n\nfunc (r *GetPermissionResp) TrTip(lang i18n.Language, requireRank int) {\n\tif r.HasPermission {\n\t\treturn\n\t}\n\tif requireRank <= 0 {\n\t\tr.NoPermissionTip = translator.Tr(lang, reason.RankFailToMeetTheCondition)\n\t} else {\n\t\tr.NoPermissionTip = translator.TrWithData(\n\t\t\tlang, reason.NoEnoughRankToOperate, &PermissionTrTplData{Rank: requireRank})\n\t}\n}\n"
  },
  {
    "path": "internal/schema/plugin_admin_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst (\n\tPluginStatusActive   PluginStatus = \"active\"\n\tPluginStatusInactive PluginStatus = \"inactive\"\n)\n\ntype PluginStatus string\n\ntype GetPluginListReq struct {\n\tStatus     PluginStatus `form:\"status\"`\n\tHaveConfig bool         `form:\"have_config\"`\n}\n\ntype GetPluginListResp struct {\n\tName        string `json:\"name\"`\n\tSlugName    string `json:\"slug_name\"`\n\tDescription string `json:\"description\"`\n\tVersion     string `json:\"version\"`\n\tEnabled     bool   `json:\"enabled\"`\n\tHaveConfig  bool   `json:\"have_config\"`\n\tLink        string `json:\"link\"`\n}\n\ntype GetAllPluginStatusResp struct {\n\tSlugName string `json:\"slug_name\"`\n\tEnabled  bool   `json:\"enabled\"`\n}\n\ntype UpdatePluginStatusReq struct {\n\tPluginSlugName string `validate:\"required,gt=1,lte=100\" json:\"plugin_slug_name\"`\n\tEnabled        bool   `json:\"enabled\"`\n}\n\ntype GetPluginConfigReq struct {\n\tPluginSlugName string `validate:\"required,gt=1,lte=100\" form:\"plugin_slug_name\"`\n}\n\ntype GetPluginConfigResp struct {\n\tName         string        `json:\"name\"`\n\tSlugName     string        `json:\"slug_name\"`\n\tDescription  string        `json:\"description\"`\n\tVersion      string        `json:\"version\"`\n\tConfigFields []ConfigField `json:\"config_fields\"`\n}\n\nfunc (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {\n\tfor _, field := range fields {\n\t\tconfigField := ConfigField{\n\t\t\tName:        field.Name,\n\t\t\tType:        string(field.Type),\n\t\t\tTitle:       field.Title.Translate(ctx),\n\t\t\tDescription: field.Description.Translate(ctx),\n\t\t\tRequired:    field.Required,\n\t\t\tValue:       field.Value,\n\t\t\tUIOptions: ConfigFieldUIOptions{\n\t\t\t\tRows:           field.UIOptions.Rows,\n\t\t\t\tInputType:      string(field.UIOptions.InputType),\n\t\t\t\tVariant:        field.UIOptions.Variant,\n\t\t\t\tClassName:      field.UIOptions.ClassName,\n\t\t\t\tFieldClassName: field.UIOptions.FieldClassName,\n\t\t\t},\n\t\t}\n\t\tconfigField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)\n\t\tconfigField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)\n\t\tconfigField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)\n\t\tif field.UIOptions.Action != nil {\n\t\t\tuiOptionAction := &UIOptionAction{\n\t\t\t\tUrl:    field.UIOptions.Action.Url,\n\t\t\t\tMethod: field.UIOptions.Action.Method,\n\t\t\t}\n\t\t\tif field.UIOptions.Action.Loading != nil {\n\t\t\t\tuiOptionAction.Loading = &LoadingAction{\n\t\t\t\t\tText:  field.UIOptions.Action.Loading.Text.Translate(ctx),\n\t\t\t\t\tState: string(field.UIOptions.Action.Loading.State),\n\t\t\t\t}\n\t\t\t}\n\t\t\tif field.UIOptions.Action.OnComplete != nil {\n\t\t\t\tuiOptionAction.OnCompleteAction = &OnCompleteAction{\n\t\t\t\t\tToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,\n\t\t\t\t\tRefreshFormConfig:  field.UIOptions.Action.OnComplete.RefreshFormConfig,\n\t\t\t\t}\n\t\t\t}\n\t\t\tconfigField.UIOptions.Action = uiOptionAction\n\t\t}\n\n\t\tfor _, option := range field.Options {\n\t\t\tconfigField.Options = append(configField.Options, ConfigFieldOption{\n\t\t\t\tLabel: option.Label.Translate(ctx),\n\t\t\t\tValue: option.Value,\n\t\t\t})\n\t\t}\n\t\tg.ConfigFields = append(g.ConfigFields, configField)\n\t}\n}\n\ntype ConfigField struct {\n\tName        string               `json:\"name\"`\n\tType        string               `json:\"type\"`\n\tTitle       string               `json:\"title\"`\n\tDescription string               `json:\"description\"`\n\tRequired    bool                 `json:\"required\"`\n\tValue       any                  `json:\"value\"`\n\tUIOptions   ConfigFieldUIOptions `json:\"ui_options\"`\n\tOptions     []ConfigFieldOption  `json:\"options,omitempty\"`\n}\n\ntype ConfigFieldUIOptions struct {\n\tPlaceholder    string          `json:\"placeholder,omitempty\"`\n\tRows           string          `json:\"rows,omitempty\"`\n\tInputType      string          `json:\"input_type,omitempty\"`\n\tLabel          string          `json:\"label,omitempty\"`\n\tAction         *UIOptionAction `json:\"action,omitempty\"`\n\tVariant        string          `json:\"variant,omitempty\"`\n\tText           string          `json:\"text,omitempty\"`\n\tClassName      string          `json:\"class_name,omitempty\"`\n\tFieldClassName string          `json:\"field_class_name,omitempty\"`\n}\n\ntype ConfigFieldOption struct {\n\tLabel string `json:\"label\"`\n\tValue string `json:\"value\"`\n}\n\ntype UIOptionAction struct {\n\tUrl              string            `json:\"url\"`\n\tMethod           string            `json:\"method,omitempty\"`\n\tLoading          *LoadingAction    `json:\"loading,omitempty\"`\n\tOnCompleteAction *OnCompleteAction `json:\"on_complete,omitempty\"`\n}\n\ntype LoadingAction struct {\n\tText  string `json:\"text\"`\n\tState string `json:\"state\"`\n}\n\ntype OnCompleteAction struct {\n\tToastReturnMessage bool `json:\"toast_return_message\"`\n\tRefreshFormConfig  bool `json:\"refresh_form_config\"`\n}\n\ntype UpdatePluginConfigReq struct {\n\tPluginSlugName string         `validate:\"required,gt=1,lte=100\" json:\"plugin_slug_name\"`\n\tConfigFields   map[string]any `json:\"config_fields\"`\n}\n"
  },
  {
    "path": "internal/schema/plugin_user_center.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\ntype UserCenterAgentResp struct {\n\tEnabled   bool       `json:\"enabled\"`\n\tAgentInfo *AgentInfo `json:\"agent_info\"`\n}\n\ntype AgentInfo struct {\n\tName                      string           `json:\"name\"`\n\tDisplayName               string           `json:\"display_name\"`\n\tIcon                      string           `json:\"icon\"`\n\tUrl                       string           `json:\"url\"`\n\tLoginRedirectURL          string           `json:\"login_redirect_url\"`\n\tSignUpRedirectURL         string           `json:\"sign_up_redirect_url\"`\n\tControlCenterItems        []*ControlCenter `json:\"control_center\"`\n\tEnabledOriginalUserSystem bool             `json:\"enabled_original_user_system\"`\n}\n\ntype ControlCenter struct {\n\tName  string `json:\"name\"`\n\tLabel string `json:\"label\"`\n\tUrl   string `json:\"url\"`\n}\n\ntype UserCenterPersonalBranding struct {\n\tEnabled          bool                `json:\"enabled\"`\n\tPersonalBranding []*PersonalBranding `json:\"personal_branding\"`\n}\n\ntype PersonalBranding struct {\n\tIcon  string `json:\"icon\"`\n\tName  string `json:\"name\"`\n\tLabel string `json:\"label\"`\n\tUrl   string `json:\"url\"`\n}\n"
  },
  {
    "path": "internal/schema/plugin_user_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype GetUserPluginListResp struct {\n\tName     string `json:\"name\"`\n\tSlugName string `json:\"slug_name\"`\n}\n\ntype UpdateUserPluginReq struct {\n\tPluginSlugName string `validate:\"required,gt=1,lte=100\" json:\"plugin_slug_name\"`\n\tUserID         string `json:\"-\"`\n}\n\ntype GetUserPluginConfigReq struct {\n\tPluginSlugName string `validate:\"required,gt=1,lte=100\" form:\"plugin_slug_name\"`\n\tUserID         string `json:\"-\"`\n}\n\ntype GetUserPluginConfigResp struct {\n\tName         string         `json:\"name\"`\n\tSlugName     string         `json:\"slug_name\"`\n\tConfigFields []*ConfigField `json:\"config_fields\"`\n}\n\nfunc (g *GetUserPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {\n\tfor _, field := range fields {\n\t\tconfigField := &ConfigField{\n\t\t\tName:        field.Name,\n\t\t\tType:        string(field.Type),\n\t\t\tTitle:       field.Title.Translate(ctx),\n\t\t\tDescription: field.Description.Translate(ctx),\n\t\t\tRequired:    field.Required,\n\t\t\tValue:       field.Value,\n\t\t\tUIOptions: ConfigFieldUIOptions{\n\t\t\t\tRows:           field.UIOptions.Rows,\n\t\t\t\tInputType:      string(field.UIOptions.InputType),\n\t\t\t\tVariant:        field.UIOptions.Variant,\n\t\t\t\tClassName:      field.UIOptions.ClassName,\n\t\t\t\tFieldClassName: field.UIOptions.FieldClassName,\n\t\t\t},\n\t\t}\n\t\tconfigField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)\n\t\tconfigField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)\n\t\tconfigField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)\n\t\tif field.UIOptions.Action != nil {\n\t\t\tuiOptionAction := &UIOptionAction{\n\t\t\t\tUrl:    field.UIOptions.Action.Url,\n\t\t\t\tMethod: field.UIOptions.Action.Method,\n\t\t\t}\n\t\t\tif field.UIOptions.Action.Loading != nil {\n\t\t\t\tuiOptionAction.Loading = &LoadingAction{\n\t\t\t\t\tText:  field.UIOptions.Action.Loading.Text.Translate(ctx),\n\t\t\t\t\tState: string(field.UIOptions.Action.Loading.State),\n\t\t\t\t}\n\t\t\t}\n\t\t\tif field.UIOptions.Action.OnComplete != nil {\n\t\t\t\tuiOptionAction.OnCompleteAction = &OnCompleteAction{\n\t\t\t\t\tToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,\n\t\t\t\t\tRefreshFormConfig:  field.UIOptions.Action.OnComplete.RefreshFormConfig,\n\t\t\t\t}\n\t\t\t}\n\t\t\tconfigField.UIOptions.Action = uiOptionAction\n\t\t}\n\n\t\tfor _, option := range field.Options {\n\t\t\tconfigField.Options = append(configField.Options, ConfigFieldOption{\n\t\t\t\tLabel: option.Label.Translate(ctx),\n\t\t\t\tValue: option.Value,\n\t\t\t})\n\t\t}\n\t\tg.ConfigFields = append(g.ConfigFields, configField)\n\t}\n}\n\ntype UpdateUserPluginConfigReq struct {\n\tPluginSlugName string         `validate:\"required,gt=1,lte=100\" json:\"plugin_slug_name\"`\n\tConfigFields   map[string]any `json:\"config_fields\"`\n\tUserID         string         `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/schema/question_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/segmentfault/pacman/errors\"\n\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\nconst (\n\tQuestionOperationPin   = \"pin\"\n\tQuestionOperationUnPin = \"unpin\"\n\tQuestionOperationHide  = \"hide\"\n\tQuestionOperationShow  = \"show\"\n)\n\n// RemoveQuestionReq delete question request\ntype RemoveQuestionReq struct {\n\t// question id\n\tID          string `validate:\"required\" json:\"id\"`\n\tUserID      string `json:\"-\" ` // user_id\n\tIsAdmin     bool   `json:\"-\"`\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\ntype CloseQuestionReq struct {\n\tID        string `validate:\"required\" json:\"id\"`\n\tCloseType int    `json:\"close_type\"` // close_type\n\tCloseMsg  string `json:\"close_msg\"`  // close_type\n\tUserID    string `json:\"-\"`          // user_id\n}\n\ntype OperationQuestionReq struct {\n\tID        string `validate:\"required\" json:\"id\"`\n\tOperation string `json:\"operation\"` // operation [pin unpin hide show]\n\tUserID    string `json:\"-\"`         // user_id\n\tCanPin    bool   `json:\"-\"`\n\tCanList   bool   `json:\"-\"`\n}\n\ntype CloseQuestionMeta struct {\n\tCloseType int    `json:\"close_type\"`\n\tCloseMsg  string `json:\"close_msg\"`\n}\n\n// ReopenQuestionReq reopen question request\ntype ReopenQuestionReq struct {\n\tQuestionID string `json:\"question_id\"`\n\tUserID     string `json:\"-\"`\n}\n\ntype QuestionAdd struct {\n\t// question title\n\tTitle string `validate:\"required,notblank,gte=6,lte=150\" json:\"title\"`\n\t// content\n\tContent string `validate:\"gte=0,lte=65535\" json:\"content\"`\n\t// html\n\tHTML string `json:\"-\"`\n\t// tags\n\tTags []*TagItem `validate:\"dive\" json:\"tags\"`\n\t// user id\n\tUserID string `json:\"-\"`\n\tQuestionPermission\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n\tIP          string `json:\"-\"`\n\tUserAgent   string `json:\"-\"`\n}\n\nfunc (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.HTML = converter.Markdown2HTML(req.Content)\n\tfor _, tag := range req.Tags {\n\t\tif len(tag.OriginalText) > 0 {\n\t\t\ttag.ParsedText = converter.Markdown2HTML(tag.OriginalText)\n\t\t}\n\t}\n\treturn nil, nil\n}\n\ntype QuestionAddByAnswer struct {\n\t// question title\n\tTitle string `validate:\"required,notblank,gte=6,lte=150\" json:\"title\"`\n\t// content\n\tContent string `validate:\"gte=0,lte=65535\" json:\"content\"`\n\t// html\n\tHTML          string `json:\"-\"`\n\tAnswerContent string `validate:\"required,notblank,gte=6,lte=65535\" json:\"answer_content\"`\n\tAnswerHTML    string `json:\"-\"`\n\t// tags\n\tTags []*TagItem `validate:\"dive\" json:\"tags\"`\n\t// user id\n\tUserID              string   `json:\"-\"`\n\tMentionUsernameList []string `validate:\"omitempty\" json:\"mention_username_list\"`\n\tQuestionPermission\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n\tIP          string `json:\"-\"`\n\tUserAgent   string `json:\"-\"`\n}\n\nfunc (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.HTML = converter.Markdown2HTML(req.Content)\n\treq.AnswerHTML = converter.Markdown2HTML(req.AnswerContent)\n\tfor _, tag := range req.Tags {\n\t\tif len(tag.OriginalText) > 0 {\n\t\t\ttag.ParsedText = converter.Markdown2HTML(tag.OriginalText)\n\t\t}\n\t}\n\tif req.AnswerHTML == \"\" {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"answer_content\",\n\t\t\tErrorMsg:   reason.AnswerContentCannotEmpty,\n\t\t})\n\t\treturn errFields, errors.BadRequest(reason.QuestionContentCannotEmpty)\n\t}\n\treturn nil, nil\n}\n\ntype QuestionPermission struct {\n\t// whether user can add it\n\tCanAdd bool `json:\"-\"`\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n\t// whether user can delete it\n\tCanDelete bool `json:\"-\"`\n\t// whether user can close it\n\tCanClose bool `json:\"-\"`\n\t// whether user can reopen it\n\tCanReopen bool `json:\"-\"`\n\t// whether user can pin it\n\tCanPin   bool `json:\"-\"`\n\tCanUnPin bool `json:\"-\"`\n\t// whether user can hide it\n\tCanHide bool `json:\"-\"`\n\tCanShow bool `json:\"-\"`\n\t// whether user can use reserved it\n\tCanUseReservedTag bool `json:\"-\"`\n\t// whether user can invite other user to answer this question\n\tCanInviteOtherToAnswer bool `json:\"-\"`\n\tCanAddTag              bool `json:\"-\"`\n\tCanRecover             bool `json:\"-\"`\n}\n\ntype CheckCanQuestionUpdate struct {\n\t// question id\n\tID string `validate:\"required\" form:\"id\"`\n\t// user id\n\tUserID  string `json:\"-\"`\n\tIsAdmin bool   `json:\"-\"`\n}\n\ntype QuestionUpdate struct {\n\t// question id\n\tID string `validate:\"required\" json:\"id\"`\n\t// question title\n\tTitle string `validate:\"required,notblank,gte=6,lte=150\" json:\"title\"`\n\t// content\n\tContent string `validate:\"gte=0,lte=65535\" json:\"content\"`\n\t// html\n\tHTML       string   `json:\"-\"`\n\tInviteUser []string `validate:\"omitempty\"  json:\"invite_user\"`\n\t// tags\n\tTags []*TagItem `validate:\"dive\" json:\"tags\"`\n\t// edit summary\n\tEditSummary string `validate:\"omitempty\" json:\"edit_summary\"`\n\t// user id\n\tUserID       string `json:\"-\"`\n\tNoNeedReview bool   `json:\"-\"`\n\tQuestionPermission\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\ntype QuestionRecoverReq struct {\n\tQuestionID string `validate:\"required\" json:\"question_id\"`\n\tUserID     string `json:\"-\"`\n}\n\ntype QuestionUpdateInviteUser struct {\n\tID         string   `validate:\"required\" json:\"id\"`\n\tInviteUser []string `validate:\"omitempty\"  json:\"invite_user\"`\n\tUserID     string   `json:\"-\"`\n\tQuestionPermission\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\nfunc (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.HTML = converter.Markdown2HTML(req.Content)\n\treturn nil, nil\n}\n\ntype QuestionBaseInfo struct {\n\tID              string `json:\"id\" `\n\tTitle           string `json:\"title\"`\n\tUrlTitle        string `json:\"url_title\"`\n\tViewCount       int    `json:\"view_count\"`\n\tAnswerCount     int    `json:\"answer_count\"`\n\tCollectionCount int    `json:\"collection_count\"`\n\tFollowCount     int    `json:\"follow_count\"`\n\tStatus          string `json:\"status\"`\n\tAcceptedAnswer  bool   `json:\"accepted_answer\"`\n}\n\ntype QuestionInfoResp struct {\n\tID                   string         `json:\"id\" `\n\tTitle                string         `json:\"title\"`\n\tUrlTitle             string         `json:\"url_title\"`\n\tContent              string         `json:\"content\"`\n\tHTML                 string         `json:\"html\"`\n\tDescription          string         `json:\"description\"`\n\tTags                 []*TagResp     `json:\"tags\"`\n\tViewCount            int            `json:\"view_count\"`\n\tUniqueViewCount      int            `json:\"unique_view_count\"`\n\tVoteCount            int            `json:\"vote_count\"`\n\tAnswerCount          int            `json:\"answer_count\"`\n\tCollectionCount      int            `json:\"collection_count\"`\n\tFollowCount          int            `json:\"follow_count\"`\n\tAcceptedAnswerID     string         `json:\"accepted_answer_id\"`\n\tLastAnswerID         string         `json:\"last_answer_id\"`\n\tCreateTime           int64          `json:\"create_time\"`\n\tUpdateTime           int64          `json:\"-\"`\n\tPostUpdateTime       int64          `json:\"update_time\"`\n\tQuestionUpdateTime   int64          `json:\"edit_time\"`\n\tPin                  int            `json:\"pin\"`\n\tShow                 int            `json:\"show\"`\n\tStatus               int            `json:\"status\"`\n\tOperation            *Operation     `json:\"operation,omitempty\"`\n\tUserID               string         `json:\"-\"`\n\tLastEditUserID       string         `json:\"-\"`\n\tLastAnsweredUserID   string         `json:\"-\"`\n\tUserInfo             *UserBasicInfo `json:\"user_info\"`\n\tUpdateUserInfo       *UserBasicInfo `json:\"update_user_info,omitempty\"`\n\tLastAnsweredUserInfo *UserBasicInfo `json:\"last_answered_user_info,omitempty\"`\n\tAnswered             bool           `json:\"answered\"`\n\tFirstAnswerId        string         `json:\"first_answer_id\"`\n\tCollected            bool           `json:\"collected\"`\n\tVoteStatus           string         `json:\"vote_status\"`\n\tIsFollowed           bool           `json:\"is_followed\"`\n\n\t// MemberActions\n\tMemberActions  []*PermissionMemberAction `json:\"member_actions\"`\n\tExtendsActions []*PermissionMemberAction `json:\"extends_actions\"`\n}\n\n// UpdateQuestionResp update question resp\ntype UpdateQuestionResp struct {\n\tUrlTitle      string `json:\"url_title\"`\n\tWaitForReview bool   `json:\"wait_for_review\"`\n}\n\ntype AdminQuestionInfo struct {\n\tID               string         `json:\"id\"`\n\tTitle            string         `json:\"title\"`\n\tVoteCount        int            `json:\"vote_count\"`\n\tShow             int            `json:\"show\"`\n\tPin              int            `json:\"pin\"`\n\tAnswerCount      int            `json:\"answer_count\"`\n\tAcceptedAnswerID string         `json:\"accepted_answer_id\"`\n\tCreateTime       int64          `json:\"create_time\"`\n\tUpdateTime       int64          `json:\"update_time\"`\n\tEditTime         int64          `json:\"edit_time\"`\n\tUserID           string         `json:\"-\" `\n\tUserInfo         *UserBasicInfo `json:\"user_info\"`\n}\n\ntype OperationLevel string\n\nconst (\n\tOperationLevelInfo      OperationLevel = \"info\"\n\tOperationLevelDanger    OperationLevel = \"danger\"\n\tOperationLevelWarning   OperationLevel = \"warning\"\n\tOperationLevelSecondary OperationLevel = \"secondary\"\n)\n\ntype Operation struct {\n\tType        string         `json:\"type\"`\n\tDescription string         `json:\"description\"`\n\tMsg         string         `json:\"msg\"`\n\tTime        int64          `json:\"time\"`\n\tLevel       OperationLevel `json:\"level\"`\n}\n\ntype GetCloseTypeResp struct {\n\t// report name\n\tName string `json:\"name\"`\n\t// report description\n\tDescription string `json:\"description\"`\n\t// report source\n\tSource string `json:\"source\"`\n\t// report type\n\tType int `json:\"type\"`\n\t// is have content\n\tHaveContent bool `json:\"have_content\"`\n\t// content type\n\tContentType string `json:\"content_type\"`\n}\n\ntype UserAnswerInfo struct {\n\tAnswerID     string `json:\"answer_id\"`\n\tQuestionID   string `json:\"question_id\"`\n\tAccepted     int    `json:\"accepted\"`\n\tVoteCount    int    `json:\"vote_count\"`\n\tCreateTime   int    `json:\"create_time\"`\n\tUpdateTime   int    `json:\"update_time\"`\n\tQuestionInfo struct {\n\t\tTitle    string `json:\"title\"`\n\t\tUrlTitle string `json:\"url_title\"`\n\t\tTags     []any  `json:\"tags\"`\n\t} `json:\"question_info\"`\n}\n\ntype UserQuestionInfo struct {\n\tID               string `json:\"question_id\"`\n\tTitle            string `json:\"title\"`\n\tUrlTitle         string `json:\"url_title\"`\n\tVoteCount        int    `json:\"vote_count\"`\n\tTags             []any  `json:\"tags\"`\n\tViewCount        int    `json:\"view_count\"`\n\tAnswerCount      int    `json:\"answer_count\"`\n\tCollectionCount  int    `json:\"collection_count\"`\n\tCreatedAt        int64  `json:\"created_at\"`\n\tAcceptedAnswerID string `json:\"accepted_answer_id\"`\n\tStatus           string `json:\"status\"`\n}\n\nconst (\n\tQuestionOrderCondNewest     = \"newest\"\n\tQuestionOrderCondActive     = \"active\"\n\tQuestionOrderCondHot        = \"hot\"\n\tQuestionOrderCondScore      = \"score\"\n\tQuestionOrderCondUnanswered = \"unanswered\"\n\tQuestionOrderCondRecommend  = \"recommend\"\n\tQuestionOrderCondFrequent   = \"frequent\"\n\n\t// HotInDays limit max days of the hottest question\n\tHotInDays = 90\n)\n\n// QuestionPageReq query questions page\ntype QuestionPageReq struct {\n\tPage      int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize  int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tOrderCond string `validate:\"omitempty,oneof=newest active hot score unanswered recommend frequent\" form:\"order\"`\n\tTag       string `validate:\"omitempty,gt=0,lte=100\" form:\"tag\"`\n\tUsername  string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\tInDays    int    `validate:\"omitempty,min=1\" form:\"in_days\"`\n\n\tLoginUserID      string `json:\"-\"`\n\tUserIDBeSearched string `json:\"-\"`\n\tTagID            string `json:\"-\"`\n\tShowPending      bool   `json:\"-\"`\n}\n\nconst (\n\tQuestionPageRespOperationTypeAsked    = \"asked\"\n\tQuestionPageRespOperationTypeAnswered = \"answered\"\n\tQuestionPageRespOperationTypeModified = \"modified\"\n)\n\ntype QuestionPageResp struct {\n\tID          string     `json:\"id\" `\n\tCreatedAt   int64      `json:\"created_at\"`\n\tTitle       string     `json:\"title\"`\n\tUrlTitle    string     `json:\"url_title\"`\n\tDescription string     `json:\"description\"`\n\tPin         int        `json:\"pin\"`  // 1: unpin, 2: pin\n\tShow        int        `json:\"show\"` // 0: show, 1: hide\n\tStatus      int        `json:\"status\"`\n\tTags        []*TagResp `json:\"tags\"`\n\n\t// question statistical information\n\tViewCount       int `json:\"view_count\"`\n\tUniqueViewCount int `json:\"unique_view_count\"`\n\tVoteCount       int `json:\"vote_count\"`\n\tAnswerCount     int `json:\"answer_count\"`\n\tCollectionCount int `json:\"collection_count\"`\n\tFollowCount     int `json:\"follow_count\"`\n\n\t// answer information\n\tAcceptedAnswerID   string    `json:\"accepted_answer_id\"`\n\tLastAnswerID       string    `json:\"last_answer_id\"`\n\tLastAnsweredUserID string    `json:\"-\"`\n\tLastAnsweredAt     time.Time `json:\"-\"`\n\n\t// operator information\n\tOperatedAt    int64                     `json:\"operated_at\"`\n\tOperator      *QuestionPageRespOperator `json:\"operator\"`\n\tOperationType string                    `json:\"operation_type\"`\n}\n\ntype QuestionPageRespOperator struct {\n\tID          string `json:\"id\"`\n\tUsername    string `json:\"username\"`\n\tRank        int    `json:\"rank\"`\n\tDisplayName string `json:\"display_name\"`\n\tStatus      string `json:\"status\"`\n\tAvatar      string `json:\"avatar\"`\n}\n\ntype AdminQuestionPageReq struct {\n\tPage        int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize    int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tStatusCond  string `validate:\"omitempty,oneof=normal closed deleted pending\" form:\"status\"`\n\tQuery       string `validate:\"omitempty,gt=0,lte=100\" json:\"query\" form:\"query\" `\n\tStatus      int    `json:\"-\"`\n\tLoginUserID string `json:\"-\"`\n}\n\nfunc (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, err error) {\n\tstatus, ok := entity.AdminQuestionSearchStatus[req.StatusCond]\n\tif ok {\n\t\treq.Status = status\n\t}\n\tif req.Status == 0 {\n\t\treq.Status = 1\n\t}\n\treturn nil, nil\n}\n\n// AdminAnswerPageReq admin answer page req\ntype AdminAnswerPageReq struct {\n\tPage          int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize      int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tStatusCond    string `validate:\"omitempty,oneof=normal deleted pending\" form:\"status\"`\n\tQuery         string `validate:\"omitempty,gt=0,lte=100\" form:\"query\"`\n\tQuestionID    string `validate:\"omitempty,gt=0,lte=24\" form:\"question_id\"`\n\tQuestionTitle string `json:\"-\"`\n\tAnswerID      string `json:\"-\"`\n\tStatus        int    `json:\"-\"`\n\tLoginUserID   string `json:\"-\"`\n}\n\nfunc (req *AdminAnswerPageReq) Check() (errField []*validator.FormErrorField, err error) {\n\treq.QuestionID = uid.DeShortID(req.QuestionID)\n\tif req.QuestionID == \"0\" {\n\t\treq.QuestionID = \"\"\n\t}\n\n\tif status, ok := entity.AdminAnswerSearchStatus[req.StatusCond]; ok {\n\t\treq.Status = status\n\t}\n\tif req.Status == 0 {\n\t\treq.Status = 1\n\t}\n\n\t// parse query condition\n\tif len(req.Query) > 0 {\n\t\tprefix := \"answer:\"\n\t\tif strings.Contains(req.Query, prefix) {\n\t\t\treq.AnswerID = uid.DeShortID(strings.TrimSpace(strings.TrimPrefix(req.Query, prefix)))\n\t\t} else {\n\t\t\treq.QuestionTitle = strings.TrimSpace(req.Query)\n\t\t}\n\t}\n\treturn nil, nil\n}\n\ntype AdminUpdateQuestionStatusReq struct {\n\tQuestionID string `validate:\"required\" json:\"question_id\"`\n\tStatus     string `validate:\"required,oneof=available closed deleted\" json:\"status\"`\n\tUserID     string `json:\"-\"`\n}\n\ntype PersonalQuestionPageReq struct {\n\tPage        int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize    int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tOrderCond   string `validate:\"omitempty,oneof=newest active hot score unanswered\" form:\"order\"`\n\tUsername    string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\tLoginUserID string `json:\"-\"`\n\tIsAdmin     bool   `json:\"-\"`\n}\n\ntype PersonalAnswerPageReq struct {\n\tPage        int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize    int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tOrderCond   string `validate:\"omitempty,oneof=newest active hot score unanswered\" form:\"order\"`\n\tUsername    string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\tLoginUserID string `json:\"-\"`\n\tIsAdmin     bool   `json:\"-\"`\n}\n\ntype PersonalCollectionPageReq struct {\n\tPage     int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n\tUserID   string `json:\"-\"`\n}\n\ntype GetQuestionLinkReq struct {\n\tPage       int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize   int    `validate:\"omitempty,min=1,max=100\" form:\"page_size\"`\n\tQuestionID string `validate:\"required\" form:\"question_id\"`\n\tOrderCond  string `validate:\"omitempty,oneof=newest active hot score unanswered recommend frequent\" form:\"order\"`\n\tInDays     int    `validate:\"omitempty,min=1\" form:\"in_days\"`\n\n\tLoginUserID string `json:\"-\"`\n}\n\ntype GetQuestionLinkResp struct {\n\tQuestionPageResp\n}\n"
  },
  {
    "path": "internal/schema/rank_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// GetRankPersonalWithPageReq get rank list page request\ntype GetRankPersonalWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// username\n\tUsername string `validate:\"omitempty,gt=0,lte=100\" form:\"username\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\n// GetRankPersonalPageResp rank response\ntype GetRankPersonalPageResp struct {\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// object id\n\tObjectID string `json:\"object_id\"`\n\t// question id\n\tQuestionID string `json:\"question_id\"`\n\t// answer id\n\tAnswerID string `json:\"answer_id\"`\n\t// object type\n\tObjectType string `json:\"object_type\" enums:\"question,answer,tag,comment\"`\n\t// title\n\tTitle string `json:\"title\"`\n\t// url title\n\tUrlTitle string `json:\"url_title\"`\n\t// content\n\tContent string `json:\"content\"`\n\t// reputation\n\tReputation int `json:\"reputation\"`\n\t// rank type\n\tRankType string `json:\"rank_type\"`\n}\n"
  },
  {
    "path": "internal/schema/reason_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/segmentfault/pacman/i18n\"\n)\n\ntype ReasonItem struct {\n\tReasonKey   string `json:\"reason_key\"`\n\tReasonType  int    `json:\"reason_type\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\tContentType string `json:\"content_type\"`\n\tPlaceholder string `json:\"placeholder\"`\n}\n\ntype ReasonReq struct {\n\t// ObjectType\n\tObjectType string `validate:\"required\" form:\"object_type\" json:\"object_type\"`\n\t// Action\n\tAction string `validate:\"required\" form:\"action\" json:\"action\"`\n}\n\nfunc (r *ReasonItem) Translate(keyPrefix string, lang i18n.Language) {\n\ttrField := func(fieldName, fieldData string) string {\n\t\t// If fieldData is empty, means no need to translate\n\t\tif len(fieldData) == 0 {\n\t\t\treturn fieldData\n\t\t}\n\t\tkey := keyPrefix + \".\" + fieldName\n\t\tfieldTr := translator.Tr(lang, key)\n\t\tif fieldTr != key {\n\t\t\t// If i18n key exists, return i18n value\n\t\t\treturn fieldTr\n\t\t}\n\t\t// If i18n key not exists, return fieldData original value\n\t\treturn fieldData\n\t}\n\n\tr.ReasonKey = keyPrefix\n\tr.Name = trField(\"name\", r.Name)\n\tr.Description = trField(\"desc\", r.Description)\n\tr.Placeholder = trField(\"placeholder\", r.Placeholder)\n}\n"
  },
  {
    "path": "internal/schema/render_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// PostRenderReq post render request\ntype PostRenderReq struct {\n\tContent string `json:\"content\"`\n}\n"
  },
  {
    "path": "internal/schema/report_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// AddReportReq add report request\ntype AddReportReq struct {\n\t// object id\n\tObjectID string `validate:\"required,gt=0,lte=20\" json:\"object_id\"`\n\t// report type\n\tReportType int `validate:\"required\" json:\"report_type\"`\n\t// report content\n\tContent string `validate:\"omitempty,gt=0,lte=500\" json:\"content\"`\n\t// user id\n\tUserID      string `json:\"-\"`\n\tCaptchaID   string `json:\"captcha_id\"` // captcha_id\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\n// GetReportListReq get report list all request\ntype GetReportListReq struct {\n\t// report source\n\tSource string `validate:\"required,oneof=question answer comment\" form:\"source\"`\n}\n\n// GetReportTypeResp get report response\ntype GetReportTypeResp struct {\n\t// report name\n\tName string `json:\"name\"`\n\t// report description\n\tDescription string `json:\"description\"`\n\t// report source\n\tSource string `json:\"source\"`\n\t// report type\n\tType int `json:\"type\"`\n\t// is have content\n\tHaveContent bool `json:\"have_content\"`\n\t// content type\n\tContentType string `json:\"content_type\"`\n}\n\n// ReportHandleReq request handle request\ntype ReportHandleReq struct {\n\tID             string `validate:\"required\" comment:\"report id\" form:\"id\" json:\"id\"`\n\tFlaggedType    int    `validate:\"required\" comment:\"flagged type\" form:\"flagged_type\" json:\"flagged_type\"`\n\tFlaggedContent string `validate:\"omitempty\" comment:\"flagged content\" form:\"flagged_content\" json:\"flagged_content\"`\n}\n\n// GetReportListPageDTO report list data transfer object\ntype GetReportListPageDTO struct {\n\tPage     int\n\tPageSize int\n\tStatus   int\n}\n\n// GetReportListPageResp get report list\ntype GetReportListPageResp struct {\n\tFlagID           string        `json:\"flag_id\"`\n\tCreatedAt        int64         `json:\"created_at\"`\n\tObjectID         string        `json:\"object_id\"`\n\tQuestionID       string        `json:\"question_id\"`\n\tAnswerID         string        `json:\"answer_id\"`\n\tCommentID        string        `json:\"comment_id\"`\n\tObjectType       string        `json:\"object_type\" enums:\"question,answer,comment\"`\n\tTitle            string        `json:\"title\"`\n\tUrlTitle         string        `json:\"url_title\"`\n\tOriginalText     string        `json:\"original_text\"`\n\tParsedText       string        `json:\"parsed_text\"`\n\tAnswerCount      int           `json:\"answer_count\"`\n\tAnswerAccepted   bool          `json:\"answer_accepted\"`\n\tTags             []*TagResp    `json:\"tags\"`\n\tObjectStatus     int           `json:\"object_status\"`\n\tObjectShowStatus int           `json:\"object_show_status\"`\n\tAuthorUserInfo   UserBasicInfo `json:\"author_user_info\"`\n\tSubmitAt         int64         `json:\"submit_at\"`\n\tSubmitterUser    UserBasicInfo `json:\"submitter_user\"`\n\tReason           *ReasonItem   `json:\"reason\"`\n\tReasonContent    string        `json:\"reason_content\"`\n}\n\n// GetUnreviewedReportPostPageReq get unreviewed report post page request\ntype GetUnreviewedReportPostPageReq struct {\n\tPage    int    `json:\"page\" form:\"page\"`\n\tUserID  string `json:\"-\"`\n\tIsAdmin bool   `json:\"-\"`\n}\n\n// ReviewReportReq review report request\ntype ReviewReportReq struct {\n\tFlagID        string     `validate:\"required\" json:\"flag_id\"`\n\tOperationType string     `validate:\"required,oneof=edit_post close_post delete_post unlist_post ignore_report\" json:\"operation_type\"`\n\tCloseType     int        `validate:\"omitempty\" json:\"close_type\"`\n\tCloseMsg      string     `validate:\"omitempty\" json:\"close_msg\"`\n\tTitle         string     `validate:\"omitempty,notblank,gte=6,lte=150\" json:\"title\"`\n\tContent       string     `validate:\"omitempty,notblank,gte=6,lte=65535\" json:\"content\"`\n\tTags          []*TagItem `validate:\"omitempty,dive\" json:\"tags\"`\n\tUserID        string     `json:\"-\"`\n\tIsAdmin       bool       `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/schema/review_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\n// UpdateReviewReq update review request\ntype UpdateReviewReq struct {\n\tReviewID int    `validate:\"required\" json:\"review_id\"`\n\tStatus   string `validate:\"required,oneof=approve reject\" json:\"status\"`\n\tUserID   string `json:\"-\"`\n\tIsAdmin  bool   `json:\"-\"`\n}\n\nfunc (r *UpdateReviewReq) IsApprove() bool {\n\treturn r.Status == \"approve\"\n}\n\nfunc (r *UpdateReviewReq) IsReject() bool {\n\treturn r.Status == \"reject\"\n}\n\n// GetUnreviewedPostPageReq get review page request\ntype GetUnreviewedPostPageReq struct {\n\tObjectID        string            `validate:\"omitempty\" form:\"object_id\"`\n\tPage            int               `validate:\"omitempty\" form:\"page\"`\n\tReviewerMapping map[string]string `json:\"-\"`\n\tUserID          string            `json:\"-\"`\n\tIsAdmin         bool              `json:\"-\"`\n}\n\nfunc (r *GetUnreviewedPostPageReq) Check() (errField []*validator.FormErrorField, err error) {\n\tif len(r.ObjectID) > 0 {\n\t\tr.Page = 1\n\t\tr.ObjectID = uid.DeShortID(r.ObjectID)\n\t}\n\treturn\n}\n\n// GetUnreviewedPostPageResp get review page response\ntype GetUnreviewedPostPageResp struct {\n\tReviewID             int           `json:\"review_id\"`\n\tCreatedAt            int64         `json:\"created_at\"`\n\tObjectID             string        `json:\"object_id\"`\n\tQuestionID           string        `json:\"question_id\"`\n\tAnswerID             string        `json:\"answer_id\"`\n\tCommentID            string        `json:\"comment_id\"`\n\tObjectType           string        `json:\"object_type\" enums:\"question,answer,comment\"`\n\tTitle                string        `json:\"title\"`\n\tUrlTitle             string        `json:\"url_title\"`\n\tOriginalText         string        `json:\"original_text\"`\n\tParsedText           string        `json:\"parsed_text\"`\n\tTags                 []*TagResp    `json:\"tags\"`\n\tObjectStatus         int           `json:\"object_status\"`\n\tObjectShowStatus     int           `json:\"object_show_status\"`\n\tAuthorUserInfo       UserBasicInfo `json:\"author_user_info\"`\n\tSubmitAt             int64         `json:\"submit_at\"`\n\tSubmitterDisplayName string        `json:\"submitter_display_name\"`\n\tReason               string        `json:\"reason\"`\n}\n"
  },
  {
    "path": "internal/schema/revision_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n)\n\n// AddRevisionDTO add revision request\ntype AddRevisionDTO struct {\n\t// user id\n\tUserID string\n\t// object id\n\tObjectID string\n\t// title\n\tTitle string\n\t// content\n\tContent string\n\t// log\n\tLog string\n\t// status\n\tStatus int\n}\n\n// GetRevisionListReq get revision list all request\ntype GetRevisionListReq struct {\n\t// object id\n\tObjectID string `validate:\"required\" comment:\"object_id\" form:\"object_id\"`\n\tIsAdmin  bool   `json:\"-\"`\n\tUserID   string `json:\"-\"`\n}\n\nconst RevisionAuditApprove = \"approve\"\nconst RevisionAuditReject = \"reject\"\n\ntype RevisionAuditReq struct {\n\t// object id\n\tID                string `validate:\"required\" comment:\"id\" form:\"id\"`\n\tOperation         string `validate:\"required\" comment:\"operation\" form:\"operation\"` // approve or reject\n\tUserID            string `json:\"-\"`\n\tCanReviewQuestion bool   `json:\"-\"`\n\tCanReviewAnswer   bool   `json:\"-\"`\n\tCanReviewTag      bool   `json:\"-\"`\n}\n\ntype RevisionSearch struct {\n\tPage              int    `json:\"page\" form:\"page\"` // Query number of pages\n\tCanReviewQuestion bool   `json:\"-\"`\n\tCanReviewAnswer   bool   `json:\"-\"`\n\tCanReviewTag      bool   `json:\"-\"`\n\tUserID            string `json:\"-\"`\n}\n\nfunc (r RevisionSearch) GetCanReviewObjectTypes() []int {\n\tobjectType := make([]int, 0)\n\tif r.CanReviewAnswer {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType])\n\t}\n\tif r.CanReviewQuestion {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType])\n\t}\n\tif r.CanReviewTag {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType])\n\t}\n\treturn objectType\n}\n\ntype GetUnreviewedRevisionResp struct {\n\tType           string                      `json:\"type\"`\n\tInfo           *UnreviewedRevisionInfoInfo `json:\"info\"`\n\tUnreviewedInfo *GetRevisionResp            `json:\"unreviewed_info\"`\n}\n\n// GetRevisionResp get revision response\ntype GetRevisionResp struct {\n\tID              string        `json:\"id\"`\n\tUserID          string        `json:\"use_id\"`\n\tObjectID        string        `json:\"object_id\"`\n\tObjectType      int           `json:\"-\"`\n\tTitle           string        `json:\"title\"`\n\tUrlTitle        string        `json:\"url_title\"`\n\tContent         string        `json:\"-\"`\n\tContentParsed   any           `json:\"content\"`\n\tStatus          int           `json:\"status\"`\n\tCreatedAt       time.Time     `json:\"-\"`\n\tCreatedAtParsed int64         `json:\"create_at\"`\n\tUserInfo        UserBasicInfo `json:\"user_info\"`\n\tLog             string        `json:\"reason\"`\n}\n\n// GetReviewingTypeReq get reviewing type request\ntype GetReviewingTypeReq struct {\n\tCanReviewQuestion bool   `json:\"-\"`\n\tCanReviewAnswer   bool   `json:\"-\"`\n\tCanReviewTag      bool   `json:\"-\"`\n\tIsAdmin           bool   `json:\"-\"`\n\tUserID            string `json:\"-\"`\n}\n\nfunc (r *GetReviewingTypeReq) GetCanReviewObjectTypes() []int {\n\tobjectType := make([]int, 0)\n\tif r.CanReviewAnswer {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType])\n\t}\n\tif r.CanReviewQuestion {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType])\n\t}\n\tif r.CanReviewTag {\n\t\tobjectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType])\n\t}\n\treturn objectType\n}\n\n// GetReviewingTypeResp get reviewing type response\ntype GetReviewingTypeResp struct {\n\tName       string `json:\"name\"`\n\tLabel      string `json:\"label\"`\n\tTodoAmount int64  `json:\"todo_amount\"`\n}\n"
  },
  {
    "path": "internal/schema/role_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// GetRoleResp get role  response\ntype GetRoleResp struct {\n\tID          int    `json:\"id\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n}\n"
  },
  {
    "path": "internal/schema/search_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/plugin\"\n)\n\ntype SearchDTO struct {\n\tQuery       string `validate:\"required,gte=1,lte=60\" form:\"q\"`\n\tPage        int    `validate:\"omitempty,min=1\" form:\"page,default=1\"`\n\tSize        int    `validate:\"omitempty,min=1,max=50\" form:\"size,default=30\"`\n\tOrder       string `validate:\"required,oneof=newest active score relevance\" form:\"order,default=relevance\" enums:\"newest,active,score,relevance\"`\n\tCaptchaID   string `form:\"captcha_id\"`\n\tCaptchaCode string `form:\"captcha_code\"`\n\tUserID      string `json:\"-\"`\n}\n\nfunc (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) {\n\t// Replace special characters.\n\t// Special characters will cause the search abnormal, such as search for \"#\" will get nearly all the content that Markdown format.\n\treplacedContent, patterns := ReplaceSearchContent(s.Query)\n\ts.Query = strings.Join(append(patterns, replacedContent), \" \")\n\n\treturn nil, nil\n}\n\nfunc ReplaceSearchContent(content string) (string, []string) {\n\t// Define the regular expressions for key:value pairs and [tag]\n\tkeyValueRegex := regexp.MustCompile(`\\w+:\\S+`)\n\ttagRegex := regexp.MustCompile(`\\[\\w+\\]`)\n\t// Define the pattern for characters to replace\n\treplaceCharsPattern := regexp.MustCompile(`[+#.<>\\-_()*]`)\n\n\t// Extract key:value pairs\n\tkeyValues := keyValueRegex.FindAllString(content, -1)\n\t// Extract [tag]\n\ttags := tagRegex.FindAllString(content, -1)\n\n\t// Replace key:value pairs and [tag] with empty string\n\tcontentWithoutPatterns := keyValueRegex.ReplaceAllString(content, \"\")\n\tcontentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, \"\")\n\n\t// Replace characters with pattern [+#.<>_()*] with space\n\treplacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, \" \")\n\n\treturn strings.TrimSpace(replacedContent), append(keyValues, tags...)\n}\n\ntype SearchCondition struct {\n\t// search target type: all/question/answer\n\tTargetType string\n\t// search query user id\n\tUserID string\n\t// vote amount\n\tVoteAmount int\n\t// only show not accepted answer's question\n\tNotAccepted bool\n\t// view amount\n\tViews int\n\t// answer count\n\tAnswerAmount int\n\t// only show accepted answer\n\tAccepted bool\n\t// only show this question's answer\n\tQuestionID string\n\t// search query tags\n\tTags [][]string\n\t// search query keywords\n\tWords []string\n}\n\n// SearchAll check if search all\nfunc (s *SearchCondition) SearchAll() bool {\n\treturn len(s.TargetType) == 0\n}\n\n// SearchQuestion check if search only need question\nfunc (s *SearchCondition) SearchQuestion() bool {\n\treturn s.TargetType == constant.QuestionObjectType\n}\n\n// SearchAnswer check if search only need answer\nfunc (s *SearchCondition) SearchAnswer() bool {\n\treturn s.TargetType == constant.AnswerObjectType\n}\n\n// Convert2PluginSearchCond convert to plugin search condition\nfunc (s *SearchCondition) Convert2PluginSearchCond(page, pageSize int, order string) *plugin.SearchBasicCond {\n\tbasic := &plugin.SearchBasicCond{\n\t\tPage:         page,\n\t\tPageSize:     pageSize,\n\t\tWords:        s.Words,\n\t\tTagIDs:       s.Tags,\n\t\tUserID:       s.UserID,\n\t\tOrder:        plugin.SearchOrderCond(order),\n\t\tQuestionID:   s.QuestionID,\n\t\tVoteAmount:   s.VoteAmount,\n\t\tViewAmount:   s.Views,\n\t\tAnswerAmount: s.AnswerAmount,\n\t}\n\tif s.Accepted {\n\t\tbasic.AnswerAccepted = plugin.AcceptedCondTrue\n\t} else {\n\t\tbasic.AnswerAccepted = plugin.AcceptedCondAll\n\t}\n\tif s.NotAccepted {\n\t\tbasic.QuestionAccepted = plugin.AcceptedCondFalse\n\t} else {\n\t\tbasic.QuestionAccepted = plugin.AcceptedCondAll\n\t}\n\treturn basic\n}\n\ntype SearchObject struct {\n\tID              string `json:\"id\"`\n\tQuestionID      string `json:\"question_id\"`\n\tTitle           string `json:\"title\"`\n\tUrlTitle        string `json:\"url_title\"`\n\tExcerpt         string `json:\"excerpt\"`\n\tCreatedAtParsed int64  `json:\"created_at\"`\n\tVoteCount       int    `json:\"vote_count\"`\n\tAccepted        bool   `json:\"accepted\"`\n\tAnswerCount     int    `json:\"answer_count\"`\n\t// user info\n\tUserInfo *SearchObjectUser `json:\"user_info\"`\n\t// tags\n\tTags []*TagResp `json:\"tags\"`\n\t// Status\n\tStatusStr string `json:\"status\"`\n}\n\ntype SearchObjectUser struct {\n\tID          string `json:\"id\"`\n\tUsername    string `json:\"username\"`\n\tDisplayName string `json:\"display_name\"`\n\tRank        int    `json:\"rank\"`\n\tStatus      string `json:\"status\"`\n}\n\ntype TagResp struct {\n\tID          string `json:\"-\"`\n\tSlugName    string `json:\"slug_name\"`\n\tDisplayName string `json:\"display_name\"`\n\t// if main tag slug name is not empty, this tag is synonymous with the main tag\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n\tRecommend       bool   `json:\"recommend\"`\n\tReserved        bool   `json:\"reserved\"`\n}\n\ntype SearchResult struct {\n\t// object_type\n\tObjectType string `json:\"object_type\"`\n\t// this object\n\tObject *SearchObject `json:\"object\"`\n}\n\ntype SearchResp struct {\n\tTotal int64 `json:\"count\"`\n\t// search response\n\tSearchResults []*SearchResult `json:\"list\"`\n}\n\ntype SearchDescResp struct {\n\tName string `json:\"name\"`\n\tIcon string `json:\"icon\"`\n\tLink string `json:\"link\"`\n}\n"
  },
  {
    "path": "internal/schema/search_schema_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReplaceSearchContent(t *testing.T) {\n\tcontent := \"user:aaa [tag] ssssfdfdf-as#fsadf\"\n\treplacedContent, patterns := ReplaceSearchContent(content)\n\tret := strings.Join(append(patterns, replacedContent), \" \")\n\n\tassert.Equal(t, \"user:aaa [tag] ssssfdfdf as fsadf\", ret)\n\n\tcontent = \"user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3\"\n\treplacedContent, patterns = ReplaceSearchContent(content)\n\tret = strings.Join(append(patterns, replacedContent), \" \")\n\n\tassert.Equal(t, \"user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf\", ret)\n}\n"
  },
  {
    "path": "internal/schema/simple_obj_info_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// SimpleObjectInfo simple object info\ntype SimpleObjectInfo struct {\n\tObjectID            string `json:\"object_id\"`\n\tObjectCreatorUserID string `json:\"object_creator_user_id\"`\n\tQuestionID          string `json:\"question_id\"`\n\tQuestionStatus      int    `json:\"question_status\"`\n\tAnswerID            string `json:\"answer_id\"`\n\tAnswerStatus        int    `json:\"answer_status\"`\n\tCommentID           string `json:\"comment_id\"`\n\tCommentStatus       int    `json:\"comment_status\"`\n\tTagID               string `json:\"tag_id\"`\n\tTagStatus           int    `json:\"tag_status\"`\n\tObjectType          string `json:\"object_type\"`\n\tTitle               string `json:\"title\"`\n\tContent             string `json:\"content\"`\n}\n\n// IsDeleted is deleted\nfunc (s *SimpleObjectInfo) IsDeleted() bool {\n\tswitch s.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\treturn s.QuestionStatus == entity.QuestionStatusDeleted\n\tcase constant.AnswerObjectType:\n\t\treturn s.AnswerStatus == entity.AnswerStatusDeleted\n\tcase constant.CommentObjectType:\n\t\treturn s.CommentStatus == entity.CommentStatusDeleted\n\tcase constant.TagObjectType:\n\t\treturn s.TagStatus == entity.TagStatusDeleted\n\t}\n\treturn false\n}\n\ntype UnreviewedRevisionInfoInfo struct {\n\tCreatedAt           int64      `json:\"created_at\"`\n\tObjectID            string     `json:\"object_id\"`\n\tQuestionID          string     `json:\"question_id\"`\n\tAnswerID            string     `json:\"answer_id\"`\n\tCommentID           string     `json:\"comment_id\"`\n\tObjectType          string     `json:\"object_type\"`\n\tObjectCreatorUserID string     `json:\"object_creator_user_id\"`\n\tTitle               string     `json:\"title\"`\n\tUrlTitle            string     `json:\"url_title\"`\n\tContent             string     `json:\"content\"`\n\tHtml                string     `json:\"html\"`\n\tAnswerCount         int        `json:\"answer_count\"`\n\tAnswerAccepted      bool       `json:\"answer_accepted\"`\n\tTags                []*TagResp `json:\"tags\"`\n\tStatus              int        `json:\"status\"`\n\tShowStatus          int        `json:\"show_status\"`\n}\n"
  },
  {
    "path": "internal/schema/siteinfo_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/mail\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// SiteGeneralReq site general request\ntype SiteGeneralReq struct {\n\tName             string `validate:\"required,sanitizer,gt=1,lte=128\" form:\"name\" json:\"name\"`\n\tShortDescription string `validate:\"omitempty,sanitizer,gt=3,lte=255\" form:\"short_description\" json:\"short_description\"`\n\tDescription      string `validate:\"omitempty,sanitizer,gt=3,lte=2000\" form:\"description\" json:\"description\"`\n\tSiteUrl          string `validate:\"required,sanitizer,gt=1,lte=512,url\" form:\"site_url\" json:\"site_url\"`\n\tContactEmail     string `validate:\"required,sanitizer,gt=1,lte=512,email\" form:\"contact_email\" json:\"contact_email\"`\n}\n\nfunc (r *SiteGeneralReq) FormatSiteUrl() {\n\tparsedUrl, err := url.Parse(r.SiteUrl)\n\tif err != nil {\n\t\treturn\n\t}\n\tr.SiteUrl = fmt.Sprintf(\"%s://%s\", parsedUrl.Scheme, parsedUrl.Host)\n\tif len(parsedUrl.Path) > 0 {\n\t\tr.SiteUrl += parsedUrl.Path\n\t\tr.SiteUrl = strings.TrimSuffix(r.SiteUrl, \"/\")\n\t}\n}\n\n// SiteInterfaceReq site interface request\ntype SiteInterfaceReq struct {\n\tLanguage string `validate:\"required,gt=1,lte=128\" form:\"language\" json:\"language\"`\n\tTimeZone string `validate:\"required,gt=1,lte=128\" form:\"time_zone\" json:\"time_zone\"`\n\t// Deperecated: use SiteUsersSettingsReq instead\n\tDefaultAvatar string `validate:\"omitempty\" json:\"-\"`\n\t// Deperecated: use SiteUsersSettingsReq instead\n\tGravatarBaseURL string `validate:\"omitempty\" json:\"-\"`\n}\n\n// SiteInterfaceSettingsReq site interface settings request\ntype SiteInterfaceSettingsReq struct {\n\tLanguage string `validate:\"required,gt=1,lte=128\" json:\"language\"`\n\tTimeZone string `validate:\"required,gt=1,lte=128\" json:\"time_zone\"`\n}\n\ntype SiteInterfaceSettingsResp SiteInterfaceSettingsReq\n\ntype SiteUsersSettingsReq struct {\n\tDefaultAvatar   string `validate:\"required,oneof=system gravatar\" json:\"default_avatar\"`\n\tGravatarBaseURL string `validate:\"omitempty\" json:\"gravatar_base_url\"`\n}\n\ntype SiteUsersSettingsResp SiteUsersSettingsReq\n\n// SiteBrandingReq site branding request\ntype SiteBrandingReq struct {\n\tLogo       string `validate:\"omitempty,gt=0,lte=512\" form:\"logo\" json:\"logo\"`\n\tMobileLogo string `validate:\"omitempty,gt=0,lte=512\" form:\"mobile_logo\" json:\"mobile_logo\"`\n\tSquareIcon string `validate:\"omitempty,gt=0,lte=512\" form:\"square_icon\" json:\"square_icon\"`\n\tFavicon    string `validate:\"omitempty,gt=0,lte=512\" form:\"favicon\" json:\"favicon\"`\n}\n\n// SiteWriteReq site write request use SiteQuestionsReq, SiteAdvancedReq and SiteTagsReq instead\ntype SiteWriteReq struct {\n\tMinimumContent                 int             `validate:\"omitempty,gte=0,lte=65535\" json:\"min_content\"`\n\tRestrictAnswer                 bool            `validate:\"omitempty\" json:\"restrict_answer\"`\n\tMinimumTags                    int             `validate:\"omitempty,gte=0,lte=5\" json:\"min_tags\"`\n\tRequiredTag                    bool            `validate:\"omitempty\" json:\"required_tag\"`\n\tRecommendTags                  []*SiteWriteTag `validate:\"omitempty,dive\" json:\"recommend_tags\"`\n\tReservedTags                   []*SiteWriteTag `validate:\"omitempty,dive\" json:\"reserved_tags\"`\n\tMaxImageSize                   int             `validate:\"omitempty,gt=0\" json:\"max_image_size\"`\n\tMaxAttachmentSize              int             `validate:\"omitempty,gt=0\" json:\"max_attachment_size\"`\n\tMaxImageMegapixel              int             `validate:\"omitempty,gt=0\" json:\"max_image_megapixel\"`\n\tAuthorizedImageExtensions      []string        `validate:\"omitempty\" json:\"authorized_image_extensions\"`\n\tAuthorizedAttachmentExtensions []string        `validate:\"omitempty\" json:\"authorized_attachment_extensions\"`\n\tUserID                         string          `json:\"-\"`\n}\n\ntype SiteWriteResp SiteWriteReq\n\n// SiteQuestionsReq site questions settings request\ntype SiteQuestionsReq struct {\n\tMinimumTags    int  `validate:\"omitempty,gte=0,lte=5\" json:\"min_tags\"`\n\tMinimumContent int  `validate:\"omitempty,gte=0,lte=65535\" json:\"min_content\"`\n\tRestrictAnswer bool `validate:\"omitempty\" json:\"restrict_answer\"`\n}\n\n// SiteAdvancedReq site advanced settings request\ntype SiteAdvancedReq struct {\n\tMaxImageSize                   int      `validate:\"omitempty,gt=0\" json:\"max_image_size\"`\n\tMaxAttachmentSize              int      `validate:\"omitempty,gt=0\" json:\"max_attachment_size\"`\n\tMaxImageMegapixel              int      `validate:\"omitempty,gt=0\" json:\"max_image_megapixel\"`\n\tAuthorizedImageExtensions      []string `validate:\"omitempty\" json:\"authorized_image_extensions\"`\n\tAuthorizedAttachmentExtensions []string `validate:\"omitempty\" json:\"authorized_attachment_extensions\"`\n}\n\n// SiteTagsReq site tags settings request\ntype SiteTagsReq struct {\n\tReservedTags  []*SiteWriteTag `validate:\"omitempty,dive\" json:\"reserved_tags\"`\n\tRecommendTags []*SiteWriteTag `validate:\"omitempty,dive\" json:\"recommend_tags\"`\n\tRequiredTag   bool            `validate:\"omitempty\" json:\"required_tag\"`\n\tUserID        string          `json:\"-\"`\n}\n\nfunc (s *SiteAdvancedResp) GetMaxImageSize() int64 {\n\tif s.MaxImageSize <= 0 {\n\t\treturn constant.DefaultMaxImageSize\n\t}\n\treturn int64(s.MaxImageSize) * 1024 * 1024\n}\n\nfunc (s *SiteAdvancedResp) GetMaxAttachmentSize() int64 {\n\tif s.MaxAttachmentSize <= 0 {\n\t\treturn constant.DefaultMaxAttachmentSize\n\t}\n\treturn int64(s.MaxAttachmentSize) * 1024 * 1024\n}\n\nfunc (s *SiteAdvancedResp) GetMaxImageMegapixel() int {\n\tif s.MaxImageMegapixel <= 0 {\n\t\treturn constant.DefaultMaxImageMegapixel\n\t}\n\treturn s.MaxImageMegapixel * 1000 * 1000\n}\n\n// SiteWriteTag site write response tag\ntype SiteWriteTag struct {\n\tSlugName    string `validate:\"required\" json:\"slug_name\"`\n\tDisplayName string `json:\"display_name\"`\n}\n\n// SiteLegalReq site branding request use SitePoliciesReq and SiteSecurityReq instead\ntype SiteLegalReq struct {\n\tTermsOfServiceOriginalText string `json:\"terms_of_service_original_text\"`\n\tTermsOfServiceParsedText   string `json:\"terms_of_service_parsed_text\"`\n\tPrivacyPolicyOriginalText  string `json:\"privacy_policy_original_text\"`\n\tPrivacyPolicyParsedText    string `json:\"privacy_policy_parsed_text\"`\n\tExternalContentDisplay     string `validate:\"required,oneof=always_display ask_before_display\" json:\"external_content_display\"`\n}\n\ntype SitePoliciesReq struct {\n\tTermsOfServiceOriginalText string `json:\"terms_of_service_original_text\"`\n\tTermsOfServiceParsedText   string `json:\"terms_of_service_parsed_text\"`\n\tPrivacyPolicyOriginalText  string `json:\"privacy_policy_original_text\"`\n\tPrivacyPolicyParsedText    string `json:\"privacy_policy_parsed_text\"`\n}\n\ntype SiteSecurityReq struct {\n\tLoginRequired          bool   `json:\"login_required\"`\n\tExternalContentDisplay string `validate:\"required,oneof=always_display ask_before_display\" json:\"external_content_display\"`\n\tCheckUpdate            bool   `validate:\"omitempty,sanitizer\" form:\"check_update\" json:\"check_update\"`\n}\n\ntype SitePoliciesResp SitePoliciesReq\ntype SiteSecurityResp SiteSecurityReq\n\n// GetSiteLegalInfoReq site site legal request\ntype GetSiteLegalInfoReq struct {\n\tInfoType string `validate:\"required,oneof=tos privacy\" form:\"info_type\"`\n}\n\nfunc (r *GetSiteLegalInfoReq) IsTOS() bool {\n\treturn r.InfoType == \"tos\"\n}\n\nfunc (r *GetSiteLegalInfoReq) IsPrivacy() bool {\n\treturn r.InfoType == \"privacy\"\n}\n\n// GetSiteLegalInfoResp get site legal info response\ntype GetSiteLegalInfoResp struct {\n\tTermsOfServiceOriginalText string `json:\"terms_of_service_original_text,omitempty\"`\n\tTermsOfServiceParsedText   string `json:\"terms_of_service_parsed_text,omitempty\"`\n\tPrivacyPolicyOriginalText  string `json:\"privacy_policy_original_text,omitempty\"`\n\tPrivacyPolicyParsedText    string `json:\"privacy_policy_parsed_text,omitempty\"`\n}\n\n// SiteUsersReq site users config request\ntype SiteUsersReq struct {\n\tDefaultAvatar          string `validate:\"required,oneof=system gravatar\" json:\"default_avatar\"`\n\tGravatarBaseURL        string `json:\"gravatar_base_url\"`\n\tAllowUpdateDisplayName bool   `json:\"allow_update_display_name\"`\n\tAllowUpdateUsername    bool   `json:\"allow_update_username\"`\n\tAllowUpdateAvatar      bool   `json:\"allow_update_avatar\"`\n\tAllowUpdateBio         bool   `json:\"allow_update_bio\"`\n\tAllowUpdateWebsite     bool   `json:\"allow_update_website\"`\n\tAllowUpdateLocation    bool   `json:\"allow_update_location\"`\n}\n\n// SiteLoginReq site login request\ntype SiteLoginReq struct {\n\tAllowNewRegistrations   bool     `json:\"allow_new_registrations\"`\n\tAllowEmailRegistrations bool     `json:\"allow_email_registrations\"`\n\tAllowPasswordLogin      bool     `json:\"allow_password_login\"`\n\tAllowEmailDomains       []string `json:\"allow_email_domains\"`\n}\n\n// SiteCustomCssHTMLReq site custom css html\ntype SiteCustomCssHTMLReq struct {\n\tCustomHead    string `validate:\"omitempty,gt=0,lte=65536\" json:\"custom_head\"`\n\tCustomCss     string `validate:\"omitempty,gt=0,lte=65536\" json:\"custom_css\"`\n\tCustomHeader  string `validate:\"omitempty,gt=0,lte=65536\" json:\"custom_header\"`\n\tCustomFooter  string `validate:\"omitempty,gt=0,lte=65536\" json:\"custom_footer\"`\n\tCustomSideBar string `validate:\"omitempty,gt=0,lte=65536\" json:\"custom_sidebar\"`\n}\n\n// SiteThemeReq site theme config\ntype SiteThemeReq struct {\n\tTheme       string         `validate:\"required,gt=0,lte=255\" json:\"theme\"`\n\tThemeConfig map[string]any `validate:\"omitempty\" json:\"theme_config\"`\n\tColorScheme string         `validate:\"omitempty,gt=0,lte=100\" json:\"color_scheme\"`\n\tLayout      string         `validate:\"omitempty,oneof=Full-width Fixed-width\" json:\"layout\"`\n}\n\ntype SiteSeoReq struct {\n\tPermalink int    `validate:\"required,lte=4,gte=0\" form:\"permalink\" json:\"permalink\"`\n\tRobots    string `validate:\"required\" form:\"robots\" json:\"robots\"`\n}\n\nfunc (s *SiteSeoResp) IsShortLink() bool {\n\treturn s.Permalink == constant.PermalinkQuestionIDAndTitleByShortID ||\n\t\ts.Permalink == constant.PermalinkQuestionIDByShortID\n}\n\n// AIPromptConfig AI prompt configuration for different languages\ntype AIPromptConfig struct {\n\tZhCN string `json:\"zh_cn\"`\n\tEnUS string `json:\"en_us\"`\n}\n\n// SiteAIReq AI configuration request\ntype SiteAIReq struct {\n\tEnabled         bool              `validate:\"omitempty\" form:\"enabled\" json:\"enabled\"`\n\tChosenProvider  string            `validate:\"omitempty,lte=50\" form:\"chosen_provider\" json:\"chosen_provider\"`\n\tSiteAIProviders []*SiteAIProvider `validate:\"omitempty,dive\" form:\"ai_providers\" json:\"ai_providers\"`\n\tPromptConfig    *AIPromptConfig   `validate:\"omitempty\" form:\"prompt_config\" json:\"prompt_config,omitempty\"`\n}\n\nfunc (s *SiteAIResp) GetProvider() *SiteAIProvider {\n\tif !s.Enabled || s.ChosenProvider == \"\" {\n\t\treturn &SiteAIProvider{}\n\t}\n\tif len(s.SiteAIProviders) == 0 {\n\t\treturn &SiteAIProvider{}\n\t}\n\tfor _, provider := range s.SiteAIProviders {\n\t\tif provider.Provider == s.ChosenProvider {\n\t\t\treturn provider\n\t\t}\n\t}\n\treturn &SiteAIProvider{}\n}\n\ntype SiteAIProvider struct {\n\tProvider string `validate:\"omitempty,lte=50\" form:\"provider\" json:\"provider\"`\n\tAPIHost  string `validate:\"omitempty,lte=512\" form:\"api_host\" json:\"api_host\"`\n\tAPIKey   string `validate:\"omitempty,lte=256\" form:\"api_key\" json:\"api_key\"`\n\tModel    string `validate:\"omitempty,lte=100\" form:\"model\" json:\"model\"`\n}\n\n// SiteAIResp AI configuration response\ntype SiteAIResp SiteAIReq\n\ntype SiteMCPReq struct {\n\tEnabled bool `validate:\"omitempty\" form:\"enabled\" json:\"enabled\"`\n}\n\ntype SiteMCPResp struct {\n\tEnabled    bool   `json:\"enabled\"`\n\tType       string `json:\"type\"`\n\tURL        string `json:\"url\"`\n\tHTTPHeader string `json:\"http_header\"`\n}\n\n// SiteGeneralResp site general response\ntype SiteGeneralResp SiteGeneralReq\n\n// SiteInterfaceResp site interface response\ntype SiteInterfaceResp SiteInterfaceReq\n\n// SiteBrandingResp site branding response\ntype SiteBrandingResp SiteBrandingReq\n\n// SiteLoginResp site login response\ntype SiteLoginResp SiteLoginReq\n\n// SiteCustomCssHTMLResp site custom css html response\ntype SiteCustomCssHTMLResp SiteCustomCssHTMLReq\n\n// SiteUsersResp site users response\ntype SiteUsersResp SiteUsersReq\n\n// SiteThemeResp site theme response\ntype SiteThemeResp struct {\n\tThemeOptions []*ThemeOption `json:\"theme_options\"`\n\tTheme        string         `json:\"theme\"`\n\tThemeConfig  map[string]any `json:\"theme_config\"`\n\tColorScheme  string         `json:\"color_scheme\"`\n\tLayout       string         `json:\"layout\"`\n}\n\nfunc (s *SiteThemeResp) TrTheme(ctx context.Context) {\n\tla := handler.GetLangByCtx(ctx)\n\tfor _, option := range s.ThemeOptions {\n\t\ttr := translator.Tr(la, option.Value)\n\t\t// if tr is equal the option value means not found translation, so use the original label\n\t\tif tr != option.Value {\n\t\t\toption.Label = tr\n\t\t}\n\t}\n}\n\n// ThemeOption get label option\ntype ThemeOption struct {\n\tLabel string `json:\"label\"`\n\tValue string `json:\"value\"`\n}\n\ntype SiteQuestionsResp SiteQuestionsReq\ntype SiteAdvancedResp SiteAdvancedReq\ntype SiteTagsResp SiteTagsReq\n\n// SiteLegalResp site write response use SitePoliciesResp and SiteSecurityResp instead\ntype SiteLegalResp SiteLegalReq\n\n// SiteLegalSimpleResp site write response\ntype SiteLegalSimpleResp struct {\n\tExternalContentDisplay string `validate:\"required,oneof=always_display ask_before_display\" json:\"external_content_display\"`\n}\n\n// SiteSeoResp site write response\ntype SiteSeoResp SiteSeoReq\n\n// SiteInfoResp get site info response\ntype SiteInfoResp struct {\n\tGeneral       *SiteGeneralResp           `json:\"general\"`\n\tInterface     *SiteInterfaceSettingsResp `json:\"interface\"`\n\tUsersSettings *SiteUsersSettingsResp     `json:\"users_settings\"`\n\tBranding      *SiteBrandingResp          `json:\"branding\"`\n\tLogin         *SiteLoginResp             `json:\"login\"`\n\tTheme         *SiteThemeResp             `json:\"theme\"`\n\tCustomCssHtml *SiteCustomCssHTMLResp     `json:\"custom_css_html\"`\n\tSiteSeo       *SiteSeoResp               `json:\"site_seo\"`\n\tSiteUsers     *SiteUsersResp             `json:\"site_users\"`\n\tAdvanced      *SiteAdvancedResp          `json:\"site_advanced\"`\n\tQuestions     *SiteQuestionsResp         `json:\"site_questions\"`\n\tTags          *SiteTagsResp              `json:\"site_tags\"`\n\tLegal         *SiteLegalSimpleResp       `json:\"site_legal\"`\n\tSecurity      *SiteSecurityResp          `json:\"site_security\"`\n\tVersion       string                     `json:\"version\"`\n\tRevision      string                     `json:\"revision\"`\n\tAIEnabled     bool                       `json:\"ai_enabled\"`\n\tMCPEnabled    bool                       `json:\"mcp_enabled\"`\n}\n\ntype TemplateSiteInfoResp struct {\n\tGeneral       *SiteGeneralResp           `json:\"general\"`\n\tInterface     *SiteInterfaceSettingsResp `json:\"interface\"`\n\tBranding      *SiteBrandingResp          `json:\"branding\"`\n\tSiteSeo       *SiteSeoResp               `json:\"site_seo\"`\n\tCustomCssHtml *SiteCustomCssHTMLResp     `json:\"custom_css_html\"`\n\tTitle         string\n\tYear          string\n\tCanonical     string\n\tJsonLD        string\n\tKeywords      string\n\tDescription   string\n}\n\n// UpdateSMTPConfigReq get smtp config request\ntype UpdateSMTPConfigReq struct {\n\tFromEmail          string `validate:\"omitempty,gt=0,lte=256\" json:\"from_email\"`\n\tFromName           string `validate:\"omitempty,gt=0,lte=256\" json:\"from_name\"`\n\tSMTPHost           string `validate:\"omitempty,gt=0,lte=256\" json:\"smtp_host\"`\n\tSMTPPort           int    `validate:\"omitempty,min=1,max=65535\" json:\"smtp_port\"`\n\tEncryption         string `validate:\"omitempty,oneof=SSL TLS\" json:\"encryption\"` // \"\" SSL TLS\n\tSMTPUsername       string `validate:\"omitempty,gt=0,lte=256\" json:\"smtp_username\"`\n\tSMTPPassword       string `validate:\"omitempty,gt=0,lte=256\" json:\"smtp_password\"`\n\tSMTPAuthentication bool   `validate:\"omitempty\" json:\"smtp_authentication\"`\n\tTestEmailRecipient string `validate:\"omitempty,email\" json:\"test_email_recipient\"`\n}\n\nfunc (r *UpdateSMTPConfigReq) Check() (errField []*validator.FormErrorField, err error) {\n\t_, err = mail.ParseAddress(r.FromName)\n\tif err == nil {\n\t\treturn append(errField, &validator.FormErrorField{\n\t\t\tErrorField: \"from_name\",\n\t\t\tErrorMsg:   reason.SMTPConfigFromNameCannotBeEmail,\n\t\t}), errors.BadRequest(reason.SMTPConfigFromNameCannotBeEmail)\n\t}\n\treturn nil, nil\n}\n\n// GetSMTPConfigResp get smtp config response\ntype GetSMTPConfigResp struct {\n\tFromEmail          string `json:\"from_email\"`\n\tFromName           string `json:\"from_name\"`\n\tSMTPHost           string `json:\"smtp_host\"`\n\tSMTPPort           int    `json:\"smtp_port\"`\n\tEncryption         string `json:\"encryption\"` // \"\" SSL TLS\n\tSMTPUsername       string `json:\"smtp_username\"`\n\tSMTPPassword       string `json:\"smtp_password\"`\n\tSMTPAuthentication bool   `json:\"smtp_authentication\"`\n}\n\n// GetManifestJsonResp get manifest json response\ntype GetManifestJsonResp struct {\n\tManifestVersion int                `json:\"manifest_version\"`\n\tVersion         string             `json:\"version\"`\n\tRevision        string             `json:\"revision\"`\n\tShortName       string             `json:\"short_name\"`\n\tName            string             `json:\"name\"`\n\tIcons           []ManifestJsonIcon `json:\"icons\"`\n\tStartUrl        string             `json:\"start_url\"`\n\tDisplay         string             `json:\"display\"`\n\tThemeColor      string             `json:\"theme_color\"`\n\tBackgroundColor string             `json:\"background_color\"`\n}\n\ntype ManifestJsonIcon struct {\n\tSrc   string `json:\"src\"`\n\tSizes string `json:\"sizes\"`\n\tType  string `json:\"type\"`\n}\n\nfunc CreateManifestJsonIcons(icon string) []ManifestJsonIcon {\n\text := filepath.Ext(icon)\n\tif ext == \"\" {\n\t\text = \"png\"\n\t} else {\n\t\text = strings.ToLower(ext[1:])\n\t}\n\ticonType := fmt.Sprintf(\"image/%s\", ext)\n\treturn []ManifestJsonIcon{\n\t\t{\n\t\t\tSrc:   icon,\n\t\t\tSizes: \"16x16\",\n\t\t\tType:  iconType,\n\t\t},\n\t\t{\n\t\t\tSrc:   icon,\n\t\t\tSizes: \"32x32\",\n\t\t\tType:  iconType,\n\t\t},\n\t\t{\n\t\t\tSrc:   icon,\n\t\t\tSizes: \"48x48\",\n\t\t\tType:  iconType,\n\t\t},\n\t\t{\n\t\t\tSrc:   icon,\n\t\t\tSizes: \"128x128\",\n\t\t\tType:  iconType,\n\t\t},\n\t}\n}\n\nconst (\n\t// PrivilegeLevel1 low\n\tPrivilegeLevel1 PrivilegeLevel = 1\n\t// PrivilegeLevel2 medium\n\tPrivilegeLevel2 PrivilegeLevel = 2\n\t// PrivilegeLevel3 high\n\tPrivilegeLevel3 PrivilegeLevel = 3\n\t// PrivilegeLevelCustom custom\n\tPrivilegeLevelCustom PrivilegeLevel = 99\n)\n\ntype PrivilegeLevel int\ntype PrivilegeOptions []*PrivilegeOption\n\nfunc (p PrivilegeOptions) Choose(level PrivilegeLevel) (option *PrivilegeOption) {\n\tfor _, op := range p {\n\t\tif op.Level == level {\n\t\t\treturn op\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetPrivilegesConfigResp get privileges config response\ntype GetPrivilegesConfigResp struct {\n\tOptions       []*PrivilegeOption `json:\"options\"`\n\tSelectedLevel PrivilegeLevel     `json:\"selected_level\"`\n}\n\n// PrivilegeOption privilege option\ntype PrivilegeOption struct {\n\tLevel      PrivilegeLevel        `json:\"level\"`\n\tLevelDesc  string                `json:\"level_desc\"`\n\tPrivileges []*constant.Privilege `validate:\"dive\" json:\"privileges\"`\n}\n\n// UpdatePrivilegesConfigReq update privileges config request\ntype UpdatePrivilegesConfigReq struct {\n\tLevel            PrivilegeLevel        `validate:\"required,min=1,max=3|eq=99\" json:\"level\"`\n\tCustomPrivileges []*constant.Privilege `validate:\"dive\" json:\"custom_privileges\"`\n}\n\nvar (\n\tDefaultPrivilegeOptions      PrivilegeOptions\n\tDefaultCustomPrivilegeOption *PrivilegeOption\n\tprivilegeOptionsLevelMapping = map[string][]int{\n\t\tconstant.RankQuestionAddKey:               {1, 1, 1},\n\t\tconstant.RankAnswerAddKey:                 {1, 1, 1},\n\t\tconstant.RankCommentAddKey:                {1, 1, 1},\n\t\tconstant.RankReportAddKey:                 {1, 1, 1},\n\t\tconstant.RankCommentVoteUpKey:             {1, 1, 1},\n\t\tconstant.RankLinkUrlLimitKey:              {1, 10, 10},\n\t\tconstant.RankQuestionVoteUpKey:            {1, 8, 15},\n\t\tconstant.RankAnswerVoteUpKey:              {1, 8, 15},\n\t\tconstant.RankQuestionVoteDownKey:          {125, 125, 125},\n\t\tconstant.RankAnswerVoteDownKey:            {125, 125, 125},\n\t\tconstant.RankInviteSomeoneToAnswerKey:     {1, 500, 1000},\n\t\tconstant.RankTagAddKey:                    {1, 750, 1500},\n\t\tconstant.RankTagEditKey:                   {1, 50, 100},\n\t\tconstant.RankQuestionEditKey:              {1, 100, 200},\n\t\tconstant.RankAnswerEditKey:                {1, 100, 200},\n\t\tconstant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000},\n\t\tconstant.RankAnswerEditWithoutReviewKey:   {1, 1000, 2000},\n\t\tconstant.RankQuestionAuditKey:             {1, 1000, 2000},\n\t\tconstant.RankAnswerAuditKey:               {1, 1000, 2000},\n\t\tconstant.RankTagAuditKey:                  {1, 2500, 5000},\n\t\tconstant.RankTagEditWithoutReviewKey:      {1, 10000, 20000},\n\t\tconstant.RankTagSynonymKey:                {1, 10000, 20000},\n\t}\n)\n\nfunc init() {\n\tDefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{\n\t\tLevel:     PrivilegeLevel1,\n\t\tLevelDesc: reason.PrivilegeLevel1Desc,\n\t}, &PrivilegeOption{\n\t\tLevel:     PrivilegeLevel2,\n\t\tLevelDesc: reason.PrivilegeLevel2Desc,\n\t}, &PrivilegeOption{\n\t\tLevel:     PrivilegeLevel3,\n\t\tLevelDesc: reason.PrivilegeLevel3Desc,\n\t})\n\n\tfor _, option := range DefaultPrivilegeOptions {\n\t\tfor _, privilege := range constant.RankAllPrivileges {\n\t\t\tif len(privilegeOptionsLevelMapping[privilege.Key]) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toption.Privileges = append(option.Privileges, &constant.Privilege{\n\t\t\t\tLabel: privilege.Label,\n\t\t\t\tValue: privilegeOptionsLevelMapping[privilege.Key][option.Level-1],\n\t\t\t\tKey:   privilege.Key,\n\t\t\t})\n\t\t}\n\t}\n\n\t// set up default custom privilege option\n\tDefaultCustomPrivilegeOption = &PrivilegeOption{\n\t\tLevel:      PrivilegeLevelCustom,\n\t\tLevelDesc:  reason.PrivilegeLevelCustomDesc,\n\t\tPrivileges: DefaultPrivilegeOptions[0].Privileges,\n\t}\n}\n"
  },
  {
    "path": "internal/schema/sitemap_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\ntype SiteMapList struct {\n\tQuestionIDs []*SiteMapQuestionInfo `json:\"question_ids\"`\n\tMaxPageNum  []int                  `json:\"max_page_num\"`\n}\n\ntype SiteMapPageList struct {\n\tPageData []*SiteMapQuestionInfo `json:\"page_data\"`\n}\n\ntype SiteMapQuestionInfo struct {\n\tID         string `json:\"id\"`\n\tTitle      string `json:\"title\"`\n\tUpdateTime string `json:\"time\"`\n}\n"
  },
  {
    "path": "internal/schema/tag_list_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// AddTagListReq add tag list request\ntype AddTagListReq struct {\n\t// tag_id\n\tTagID int64 `validate:\"required\" comment:\"tag_id\" json:\"tag_id\"`\n\t// object_id\n\tObjectID int64 `validate:\"required\" comment:\"object_id\" json:\"object_id\"`\n\t// tag_list_status(available: 1; deleted: 10)\n\tStatus int `validate:\"required\" comment:\"tag_list_status(available: 1; deleted: 10)\" json:\"status\"`\n}\n\n// RemoveTagListReq delete tag list request\ntype RemoveTagListReq struct {\n\t// tag_list_id\n\tID int64 `validate:\"required\" comment:\"tag_list_id\" json:\"id\"`\n}\n\n// UpdateTagListReq update tag list request\ntype UpdateTagListReq struct {\n\t// tag_list_id\n\tID int64 `validate:\"required\" comment:\"tag_list_id\" json:\"id\"`\n\t// tag_id\n\tTagID int64 `validate:\"omitempty\" comment:\"tag_id\" json:\"tag_id\"`\n\t// object_id\n\tObjectID int64 `validate:\"omitempty\" comment:\"object_id\" json:\"object_id\"`\n\t// tag_list_status(available: 1; deleted: 10)\n\tStatus int `validate:\"omitempty\" comment:\"tag_list_status(available: 1; deleted: 10)\" json:\"status\"`\n}\n\n// GetTagListListReq get tag list list all request\ntype GetTagListListReq struct {\n\t// tag_id\n\tTagID int64 `validate:\"omitempty\" comment:\"tag_id\" form:\"tag_id\"`\n\t// object_id\n\tObjectID int64 `validate:\"omitempty\" comment:\"object_id\" form:\"object_id\"`\n\t// tag_list_status(available: 1; deleted: 10)\n\tStatus int `validate:\"omitempty\" comment:\"tag_list_status(available: 1; deleted: 10)\" form:\"status\"`\n}\n\n// GetTagListWithPageReq get tag list list page request\ntype GetTagListWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// tag_id\n\tTagID int64 `validate:\"omitempty\" comment:\"tag_id\" form:\"tag_id\"`\n\t// object_id\n\tObjectID int64 `validate:\"omitempty\" comment:\"object_id\" form:\"object_id\"`\n\t// tag_list_status(available: 1; deleted: 10)\n\tStatus int `validate:\"omitempty\" comment:\"tag_list_status(available: 1; deleted: 10)\" form:\"status\"`\n}\n\n// GetTagListResp get tag list response\ntype GetTagListResp struct {\n\t// tag_list_id\n\tID int64 `json:\"id\"`\n\t// tag_id\n\tTagID int64 `json:\"tag_id\"`\n\t// object_id\n\tObjectID int64 `json:\"object_id\"`\n\t// tag_list_status(available: 1; deleted: 10)\n\tStatus int `json:\"status\"`\n}\n"
  },
  {
    "path": "internal/schema/tag_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/pkg/converter\"\n)\n\n// SearchTagLikeReq get tag list all request\ntype SearchTagLikeReq struct {\n\t// tag\n\tTag     string `validate:\"omitempty\" form:\"tag\"`\n\tIsAdmin bool   `json:\"-\"`\n}\n\n// SearchTagsBySlugName search tags by slug name\ntype SearchTagsBySlugName struct {\n\t// slug name list split by ','\n\tTags string `form:\"tags\"`\n}\n\n// GetTagInfoReq get tag info request\ntype GetTagInfoReq struct {\n\t// tag id\n\tID string `validate:\"omitempty\" form:\"id\"`\n\t// tag slug name\n\tName       string `validate:\"omitempty,gt=0,lte=35\" form:\"name\"`\n\tUserID     string `json:\"-\"`\n\tCanEdit    bool   `json:\"-\"`\n\tCanDelete  bool   `json:\"-\"`\n\tCanMerge   bool   `json:\"-\"`\n\tCanRecover bool   `json:\"-\"`\n}\n\ntype GetTamplateTagInfoReq struct {\n\t// tag id\n\tID string `validate:\"omitempty\" form:\"id\"`\n\t// tag slug name\n\tName string `validate:\"omitempty\" form:\"name\"`\n\t// user id\n\tUserID   string `json:\"-\"`\n\tPage     int    `validate:\"omitempty,min=1\" form:\"page\"`\n\tPageSize int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n}\n\nfunc (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tr.Name = strings.ToLower(r.Name)\n\treturn nil, nil\n}\n\n// GetTagResp get tag response\ntype GetTagResp struct {\n\tTagID         string                    `json:\"tag_id\"`\n\tCreatedAt     int64                     `json:\"created_at\"`\n\tUpdatedAt     int64                     `json:\"updated_at\"`\n\tSlugName      string                    `json:\"slug_name\"`\n\tDisplayName   string                    `json:\"display_name\"`\n\tExcerpt       string                    `json:\"excerpt\"`\n\tOriginalText  string                    `json:\"original_text\"`\n\tParsedText    string                    `json:\"parsed_text\"`\n\tDescription   string                    `json:\"description\"`\n\tFollowCount   int                       `json:\"follow_count\"`\n\tQuestionCount int                       `json:\"question_count\"`\n\tIsFollower    bool                      `json:\"is_follower\"`\n\tStatus        string                    `json:\"status\"`\n\tMemberActions []*PermissionMemberAction `json:\"member_actions\"`\n\t// if main tag slug name is not empty, this tag is synonymous with the main tag\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n\tRecommend       bool   `json:\"recommend\"`\n\tReserved        bool   `json:\"reserved\"`\n}\n\nfunc (tr *GetTagResp) GetExcerpt() {\n\texcerpt := strings.TrimSpace(tr.ParsedText)\n\tidx := strings.Index(excerpt, \"\\n\")\n\tif idx >= 0 {\n\t\texcerpt = excerpt[0:idx]\n\t}\n\ttr.Excerpt = excerpt\n}\n\n// GetTagPageResp get tag response\ntype GetTagPageResp struct {\n\t// tag_id\n\tTagID string `json:\"tag_id\"`\n\t// slug_name\n\tSlugName string `json:\"slug_name\"`\n\t// display_name\n\tDisplayName string `json:\"display_name\"`\n\t// excerpt\n\tExcerpt string `json:\"excerpt\"`\n\t// description\n\tDescription string `json:\"description\"`\n\t// original text\n\tOriginalText string `json:\"original_text\"`\n\t// parsed_text\n\tParsedText string `json:\"parsed_text\"`\n\t// follower amount\n\tFollowCount int `json:\"follow_count\"`\n\t// question amount\n\tQuestionCount int `json:\"question_count\"`\n\t// is follower\n\tIsFollower bool `json:\"is_follower\"`\n\t// created time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// updated time\n\tUpdatedAt int64 `json:\"updated_at\"`\n\tRecommend bool  `json:\"recommend\"`\n\tReserved  bool  `json:\"reserved\"`\n}\n\nfunc (tr *GetTagPageResp) GetExcerpt() {\n\texcerpt := strings.TrimSpace(tr.ParsedText)\n\tidx := strings.Index(excerpt, \"\\n\")\n\tif idx >= 0 {\n\t\texcerpt = excerpt[0:idx]\n\t}\n\ttr.Excerpt = excerpt\n}\n\ntype TagChange struct {\n\tObjectID string     `json:\"object_id\"` // object_id\n\tTags     []*TagItem `json:\"tags\"`      // tags name\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\ntype TagItem struct {\n\t// slug_name\n\tSlugName string `validate:\"omitempty,gt=0,lte=35\" json:\"slug_name\"`\n\t// display_name\n\tDisplayName string `validate:\"omitempty,gt=0,lte=35\" json:\"display_name\"`\n\t// original text\n\tOriginalText string `validate:\"omitempty\" json:\"original_text\"`\n\t// parsed text\n\tParsedText string `json:\"-\"`\n}\n\n// RemoveTagReq delete tag request\ntype RemoveTagReq struct {\n\t// tag_id\n\tTagID string `validate:\"required\" json:\"tag_id\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\n// AddTagReq add tag request\ntype AddTagReq struct {\n\t// slug_name\n\tSlugName string `validate:\"required,gt=0,lte=35\" json:\"slug_name\"`\n\t// display_name\n\tDisplayName string `validate:\"required,gt=0,lte=35\" json:\"display_name\"`\n\t// original text\n\tOriginalText string `validate:\"required,gt=0,lte=65536\" json:\"original_text\"`\n\t// parsed text\n\tParsedText string `json:\"-\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\nfunc (req *AddTagReq) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.ParsedText = converter.Markdown2HTML(req.OriginalText)\n\treq.SlugName = strings.ToLower(req.SlugName)\n\treturn nil, nil\n}\n\n// AddTagResp add tag response\ntype AddTagResp struct {\n\tSlugName string `json:\"slug_name\"`\n}\n\n// UpdateTagReq update tag request\ntype UpdateTagReq struct {\n\t// tag_id\n\tTagID string `validate:\"required\" json:\"tag_id\"`\n\t// slug_name\n\tSlugName string `validate:\"omitempty,gt=0,lte=35\" json:\"slug_name\"`\n\t// display_name\n\tDisplayName string `validate:\"omitempty,gt=0,lte=35\" json:\"display_name\"`\n\t// original text\n\tOriginalText string `validate:\"omitempty\" json:\"original_text\"`\n\t// parsed text\n\tParsedText string `json:\"-\"`\n\t// edit summary\n\tEditSummary string `validate:\"omitempty\" json:\"edit_summary\"`\n\t// user id\n\tUserID       string `json:\"-\"`\n\tNoNeedReview bool   `json:\"-\"`\n}\n\nfunc (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tr.ParsedText = converter.Markdown2HTML(r.OriginalText)\n\treturn nil, nil\n}\n\n// RecoverTagReq update tag request\ntype RecoverTagReq struct {\n\tTagID  string `validate:\"required\" json:\"tag_id\"`\n\tUserID string `json:\"-\"`\n}\n\n// UpdateTagResp update tag response\ntype UpdateTagResp struct {\n\tWaitForReview bool `json:\"wait_for_review\"`\n}\n\n// GetTagWithPageReq get tag list page request\ntype GetTagWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// slug_name\n\tSlugName string `validate:\"omitempty,gt=0,lte=35\" form:\"slug_name\"`\n\t// display_name\n\tDisplayName string `validate:\"omitempty,gt=0,lte=35\" form:\"display_name\"`\n\t// query condition\n\tQueryCond string `validate:\"omitempty,oneof=popular name newest\" form:\"query_cond\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\n// GetTagSynonymsReq get tag synonyms request\ntype GetTagSynonymsReq struct {\n\t// tag_id\n\tTagID string `validate:\"required\" form:\"tag_id\"`\n\t// user id\n\tUserID string `json:\"-\"`\n\t// whether user can edit it\n\tCanEdit bool `json:\"-\"`\n}\n\n// GetTagSynonymsResp get tag synonyms response\ntype GetTagSynonymsResp struct {\n\t// synonyms\n\tSynonyms []*TagSynonym `json:\"synonyms\"`\n\t// MemberActions\n\tMemberActions []*PermissionMemberAction `json:\"member_actions\"`\n}\n\ntype TagSynonym struct {\n\t// tag id\n\tTagID string `json:\"tag_id\"`\n\t// slug name\n\tSlugName string `json:\"slug_name\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// if main tag slug name is not empty, this tag is synonymous with the main tag\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n}\n\n// UpdateTagSynonymReq update tag request\ntype UpdateTagSynonymReq struct {\n\t// tag_id\n\tTagID string `validate:\"required\" json:\"tag_id\"`\n\t// synonym tag list\n\tSynonymTagList []*TagItem `validate:\"required,dive\" json:\"synonym_tag_list\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\nfunc (req *UpdateTagSynonymReq) Format() {\n\tfor _, item := range req.SynonymTagList {\n\t\titem.SlugName = strings.ToLower(item.SlugName)\n\t}\n}\n\n// GetFollowingTagsResp get following tags response\ntype GetFollowingTagsResp struct {\n\t// tag id\n\tTagID string `json:\"tag_id\"`\n\t// slug name\n\tSlugName string `json:\"slug_name\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// if main tag slug name is not empty, this tag is synonymous with the main tag\n\tMainTagSlugName string `json:\"main_tag_slug_name\"`\n\tRecommend       bool   `json:\"recommend\"`\n\tReserved        bool   `json:\"reserved\"`\n}\n\n// GetTagBasicResp get tag basic response\ntype GetTagBasicResp struct {\n\tTagID       string `json:\"tag_id\"`\n\tSlugName    string `json:\"slug_name\"`\n\tDisplayName string `json:\"display_name\"`\n\tRecommend   bool   `json:\"recommend\"`\n\tReserved    bool   `json:\"reserved\"`\n}\n\n// MergeTagReq merge tag request\ntype MergeTagReq struct {\n\t// source tag id\n\tSourceTagID string `validate:\"required\" json:\"source_tag_id\"`\n\t// target tag id\n\tTargetTagID string `validate:\"required\" json:\"target_tag_id\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\n// MergeTagResp merge tag response\ntype MergeTagResp struct {\n}\n"
  },
  {
    "path": "internal/schema/template_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport \"time\"\n\ntype Paginator struct {\n\tPages      []int\n\tTotalpages int\n\tPrevpage   int\n\tNextpage   int\n\tCurrpage   int\n}\n\ntype QAPageJsonLD struct {\n\tContext    string `json:\"@context\"`\n\tType       string `json:\"@type\"`\n\tMainEntity struct {\n\t\tType        string    `json:\"@type\"`\n\t\tName        string    `json:\"name\"`\n\t\tText        string    `json:\"text\"`\n\t\tAnswerCount int       `json:\"answerCount\"`\n\t\tUpvoteCount int       `json:\"upvoteCount\"`\n\t\tDateCreated time.Time `json:\"dateCreated\"`\n\t\tAuthor      struct {\n\t\t\tURL  string `json:\"url\"`\n\t\t\tType string `json:\"@type\"`\n\t\t\tName string `json:\"name\"`\n\t\t} `json:\"author\"`\n\t\tAcceptedAnswer  *AcceptedAnswerItem    `json:\"acceptedAnswer,omitempty\"`\n\t\tSuggestedAnswer []*SuggestedAnswerItem `json:\"suggestedAnswer\"`\n\t} `json:\"mainEntity\"`\n}\n\ntype AcceptedAnswerItem struct {\n\tType        string    `json:\"@type\"`\n\tText        string    `json:\"text\"`\n\tDateCreated time.Time `json:\"dateCreated\"`\n\tUpvoteCount int       `json:\"upvoteCount\"`\n\tURL         string    `json:\"url\"`\n\tAuthor      struct {\n\t\tURL  string `json:\"url\"`\n\t\tType string `json:\"@type\"`\n\t\tName string `json:\"name\"`\n\t} `json:\"author\"`\n}\n\ntype SuggestedAnswerItem struct {\n\tType        string    `json:\"@type\"`\n\tText        string    `json:\"text\"`\n\tDateCreated time.Time `json:\"dateCreated\"`\n\tUpvoteCount int       `json:\"upvoteCount\"`\n\tURL         string    `json:\"url\"`\n\tAuthor      struct {\n\t\tURL  string `json:\"url\"`\n\t\tType string `json:\"@type\"`\n\t\tName string `json:\"name\"`\n\t} `json:\"author\"`\n}\n"
  },
  {
    "path": "internal/schema/theme_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nvar GetThemeOptions = []*ThemeOption{\n\t{\n\t\tLabel: \"Default\",\n\t\tValue: \"default\",\n\t},\n}\n"
  },
  {
    "path": "internal/schema/user_external_login_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\n// UserExternalLoginResp user external login resp\ntype UserExternalLoginResp struct {\n\tBindingKey  string `json:\"binding_key\"`\n\tAccessToken string `json:\"access_token\"`\n\t// ErrMsg error message, if not empty, means login failed and this message should be displayed.\n\tErrMsg   string `json:\"-\"`\n\tErrTitle string `json:\"-\"`\n}\n\n// ExternalLoginBindingUserSendEmailReq external login binding user request\ntype ExternalLoginBindingUserSendEmailReq struct {\n\tBindingKey string `validate:\"required,gt=1,lte=100\" json:\"binding_key\"`\n\tEmail      string `validate:\"required,gt=1,lte=512,email\" json:\"email\"`\n\t// If must is true, whatever email if exists, try to bind user.\n\t// If must is false, when email exist, will only be prompted with a warning.\n\tMust bool `json:\"must\"`\n}\n\n// ExternalLoginBindingUserSendEmailResp external login binding user response\ntype ExternalLoginBindingUserSendEmailResp struct {\n\tEmailExistAndMustBeConfirmed bool   `json:\"email_exist_and_must_be_confirmed\"`\n\tAccessToken                  string `json:\"access_token\"`\n}\n\n// ExternalLoginBindingUserReq external login binding user request\ntype ExternalLoginBindingUserReq struct {\n\tCode    string `validate:\"required,gt=0,lte=500\" json:\"code\"`\n\tContent string `json:\"-\"`\n}\n\n// ExternalLoginBindingUserResp external login binding user response\ntype ExternalLoginBindingUserResp struct {\n\tAccessToken string `json:\"access_token\"`\n}\n\n// ExternalLoginUserInfoCache external login user info\ntype ExternalLoginUserInfoCache struct {\n\t// Third party identification\n\t// e.g. facebook, twitter, instagram\n\tProvider string\n\t// required. The unique user ID provided by the third-party login\n\tExternalID string\n\t// optional. This name is used preferentially during registration\n\tDisplayName string\n\t// optional. This username is used preferentially during registration\n\tUsername string\n\t// optional. If email exist will bind the existing user\n\tEmail string\n\t// optional. The avatar URL provided by the third-party login platform\n\tAvatar string\n\t// optional. The original user information provided by the third-party login platform\n\tMetaInfo string\n\t// optional. The bio provided by the third-party login platform\n\tBio string\n}\n\n// ExternalLoginUnbindingReq external login unbinding user\ntype ExternalLoginUnbindingReq struct {\n\tExternalID string `validate:\"required,gt=0,lte=128\" json:\"external_id\"`\n\tUserID     string `json:\"-\"`\n}\n\n// UserCenterUserSettingsResp user center user info response\ntype UserCenterUserSettingsResp struct {\n\tProfileSettingAgent UserSettingAgent `json:\"profile_setting_agent\"`\n\tAccountSettingAgent UserSettingAgent `json:\"account_setting_agent\"`\n}\n\ntype UserCenterAdminFunctionAgentResp struct {\n\tAllowCreateUser         bool `json:\"allow_create_user\"`\n\tAllowUpdateUserStatus   bool `json:\"allow_update_user_status\"`\n\tAllowUpdateUserPassword bool `json:\"allow_update_user_password\"`\n\tAllowUpdateUserRole     bool `json:\"allow_update_user_role\"`\n}\n\ntype UserSettingAgent struct {\n\tEnabled     bool   `json:\"enabled\"`\n\tRedirectURL string `json:\"redirect_url\"`\n}\n"
  },
  {
    "path": "internal/schema/user_notification_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n)\n\ntype NotificationChannelConfig struct {\n\tKey    constant.NotificationChannelKey `json:\"key\"`\n\tEnable bool                            `json:\"enable\"`\n}\n\ntype NotificationChannels []*NotificationChannelConfig\n\nfunc NewNotificationChannelsFormJson(jsonStr string) NotificationChannels {\n\tvar list NotificationChannels\n\t_ = json.Unmarshal([]byte(jsonStr), &list)\n\treturn list\n}\n\nfunc NewNotificationChannelConfigFormJson(jsonStr string) NotificationChannelConfig {\n\tvar list NotificationChannels\n\t_ = json.Unmarshal([]byte(jsonStr), &list)\n\tif len(list) > 0 {\n\t\treturn *list[0]\n\t}\n\treturn NotificationChannelConfig{}\n}\n\nfunc (n *NotificationChannels) ToJsonString() string {\n\tdata, _ := json.Marshal(n)\n\treturn string(data)\n}\n\ntype NotificationConfig struct {\n\tInbox                          NotificationChannelConfig `json:\"inbox\"`\n\tAllNewQuestion                 NotificationChannelConfig `json:\"all_new_question\"`\n\tAllNewQuestionForFollowingTags NotificationChannelConfig `json:\"all_new_question_for_following_tags\"`\n}\n\nfunc NewNotificationConfig(configs []*entity.UserNotificationConfig) NotificationConfig {\n\tnc := NotificationConfig{}\n\tfor _, item := range configs {\n\t\tswitch item.Source {\n\t\tcase string(constant.InboxSource):\n\t\t\tnc.Inbox = NewNotificationChannelConfigFormJson(item.Channels)\n\t\tcase string(constant.AllNewQuestionSource):\n\t\t\tnc.AllNewQuestion = NewNotificationChannelConfigFormJson(item.Channels)\n\t\tcase string(constant.AllNewQuestionForFollowingTagsSource):\n\t\t\tnc.AllNewQuestionForFollowingTags = NewNotificationChannelConfigFormJson(item.Channels)\n\t\t}\n\t}\n\treturn nc\n}\n\nfunc (n *NotificationConfig) Format() {\n\tif n.Inbox.Key == \"\" {\n\t\tn.Inbox.Key = constant.EmailChannel\n\t\tn.Inbox.Enable = false\n\t}\n\tif n.AllNewQuestion.Key == \"\" {\n\t\tn.AllNewQuestion.Key = constant.EmailChannel\n\t\tn.AllNewQuestion.Enable = false\n\t}\n\tif n.AllNewQuestionForFollowingTags.Key == \"\" {\n\t\tn.AllNewQuestionForFollowingTags.Key = constant.EmailChannel\n\t\tn.AllNewQuestionForFollowingTags.Enable = false\n\t}\n}\n\n// UpdateUserNotificationConfigReq update user notification config request\ntype UpdateUserNotificationConfigReq struct {\n\tNotificationConfig\n\tUserID string `json:\"-\"`\n}\n\n// GetUserNotificationConfigResp get user notification config response\ntype GetUserNotificationConfigResp struct {\n\tNotificationConfig\n}\n"
  },
  {
    "path": "internal/schema/user_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/pkg/day\"\n\t\"github.com/segmentfault/pacman/errors\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/jinzhu/copier\"\n)\n\n// UserVerifyEmailReq user verify email request\ntype UserVerifyEmailReq struct {\n\t// code\n\tCode string `validate:\"required,gt=0,lte=500\" form:\"code\"`\n\t// content\n\tContent string `json:\"-\"`\n}\n\n// UserLoginResp get user response\ntype UserLoginResp struct {\n\t// user id\n\tID string `json:\"id\"`\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last login date\n\tLastLoginDate int64 `json:\"last_login_date\"`\n\t// username\n\tUsername string `json:\"username\"`\n\t// email\n\tEMail string `json:\"e_mail\"`\n\t// mail status(1 pass 2 to be verified)\n\tMailStatus int `json:\"mail_status\"`\n\t// notice status(1 on 2off)\n\tNoticeStatus int `json:\"notice_status\"`\n\t// follow count\n\tFollowCount int `json:\"follow_count\"`\n\t// answer count\n\tAnswerCount int `json:\"answer_count\"`\n\t// question count\n\tQuestionCount int `json:\"question_count\"`\n\t// rank\n\tRank int `json:\"rank\"`\n\t// authority group\n\tAuthorityGroup int `json:\"authority_group\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// avatar\n\tAvatar string `json:\"avatar\"`\n\t// mobile\n\tMobile string `json:\"mobile\"`\n\t// bio markdown\n\tBio string `json:\"bio\"`\n\t// bio html\n\tBioHTML string `json:\"bio_html\"`\n\t// website\n\tWebsite string `json:\"website\"`\n\t// location\n\tLocation string `json:\"location\"`\n\t// language\n\tLanguage string `json:\"language\"`\n\t// Color scheme\n\tColorScheme string `json:\"color_scheme\"`\n\t// access token\n\tAccessToken string `json:\"access_token\"`\n\t// role id\n\tRoleID int `json:\"role_id\"`\n\t// user status\n\tStatus string `json:\"status\"`\n\t// user have password\n\tHavePassword bool `json:\"have_password\"`\n\t// visit token\n\tVisitToken string `json:\"visit_token\"`\n\t// suspended until timestamp\n\tSuspendedUntil int64 `json:\"suspended_until\"`\n}\n\nfunc (r *UserLoginResp) ConvertFromUserEntity(userInfo *entity.User) {\n\t_ = copier.Copy(r, userInfo)\n\tr.CreatedAt = userInfo.CreatedAt.Unix()\n\tr.LastLoginDate = userInfo.LastLoginDate.Unix()\n\tr.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)\n\tr.HavePassword = len(userInfo.Pass) > 0\n\tif !userInfo.SuspendedUntil.IsZero() {\n\t\tr.SuspendedUntil = userInfo.SuspendedUntil.Unix()\n\t}\n}\n\ntype GetCurrentLoginUserInfoResp struct {\n\t*UserLoginResp\n\tAvatar *AvatarInfo `json:\"avatar\"`\n}\n\nfunc (r *GetCurrentLoginUserInfoResp) ConvertFromUserEntity(userInfo *entity.User) {\n\t_ = copier.Copy(r, userInfo)\n\tr.CreatedAt = userInfo.CreatedAt.Unix()\n\tr.LastLoginDate = userInfo.LastLoginDate.Unix()\n\tr.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)\n\tif len(r.ColorScheme) == 0 {\n\t\tr.ColorScheme = constant.ColorSchemeDefault\n\t}\n\tif !userInfo.SuspendedUntil.IsZero() {\n\t\tr.SuspendedUntil = userInfo.SuspendedUntil.Unix()\n\t}\n}\n\n// GetOtherUserInfoByUsernameResp get user response\ntype GetOtherUserInfoByUsernameResp struct {\n\t// user id\n\tID string `json:\"id\"`\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last login date\n\tLastLoginDate int64 `json:\"last_login_date\"`\n\t// username\n\tUsername string `json:\"username\"`\n\t// email\n\t// follow count\n\tFollowCount int `json:\"follow_count\"`\n\t// answer count\n\tAnswerCount int `json:\"answer_count\"`\n\t// question count\n\tQuestionCount int `json:\"question_count\"`\n\t// rank\n\tRank int `json:\"rank\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// avatar\n\tAvatar string `json:\"avatar\"`\n\t// mobile\n\tMobile string `json:\"mobile\"`\n\t// bio markdown\n\tBio string `json:\"bio\"`\n\t// bio html\n\tBioHTML string `json:\"bio_html\"`\n\t// website\n\tWebsite string `json:\"website\"`\n\t// location\n\tLocation  string `json:\"location\"`\n\tStatus    string `json:\"status\"`\n\tStatusMsg string `json:\"status_msg,omitempty\"`\n\t// suspended until timestamp\n\tSuspendedUntil int64 `json:\"suspended_until\"`\n}\n\nfunc (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntity(userInfo *entity.User) {\n\t_ = copier.Copy(r, userInfo)\n\tr.CreatedAt = userInfo.CreatedAt.Unix()\n\tr.LastLoginDate = userInfo.LastLoginDate.Unix()\n\tr.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)\n\tif !userInfo.SuspendedUntil.IsZero() {\n\t\tr.SuspendedUntil = userInfo.SuspendedUntil.Unix()\n\t}\n\tr.StatusMsg = \"\"\n}\n\nfunc (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntityWithLang(ctx context.Context, userInfo *entity.User) {\n\t_ = copier.Copy(r, userInfo)\n\tr.CreatedAt = userInfo.CreatedAt.Unix()\n\tr.LastLoginDate = userInfo.LastLoginDate.Unix()\n\tr.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)\n\n\tlang := handler.GetLangByCtx(ctx)\n\tif userInfo.MailStatus == entity.EmailStatusToBeVerified {\n\t\tr.StatusMsg = translator.Tr(lang, reason.UserStatusInactive)\n\t}\n\tswitch userInfo.Status {\n\tcase entity.UserStatusSuspended:\n\t\tif userInfo.SuspendedUntil.IsZero() || userInfo.SuspendedUntil.Year() >= 2099 {\n\t\t\tr.StatusMsg = translator.Tr(lang, reason.UserStatusSuspendedForever)\n\t\t} else {\n\t\t\tr.SuspendedUntil = userInfo.SuspendedUntil.Unix()\n\t\t\ttrans := translator.GlobalTrans.Tr(lang, \"ui.dates.long_date_with_time\")\n\t\t\tsuspendedUntilFormatted := day.Format(userInfo.SuspendedUntil.Unix(), trans, \"UTC\")\n\t\t\tr.StatusMsg = translator.TrWithData(lang, reason.UserStatusSuspendedUntil, map[string]any{\n\t\t\t\t\"SuspendedUntil\": suspendedUntilFormatted,\n\t\t\t})\n\t\t}\n\tcase entity.UserStatusDeleted:\n\t\tr.StatusMsg = translator.Tr(lang, reason.UserStatusDeleted)\n\t}\n}\n\n// UserEmailLoginReq user email login request\ntype UserEmailLoginReq struct {\n\tEmail       string `validate:\"required,email,gt=0,lte=500\" json:\"e_mail\"`\n\tPass        string `validate:\"required,gte=8,lte=32\" json:\"pass\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\n// UserRegisterReq user register request\ntype UserRegisterReq struct {\n\tName        string `validate:\"required,gte=2,lte=30\" json:\"name\"`\n\tEmail       string `validate:\"required,email,gt=0,lte=500\" json:\"e_mail\" `\n\tPass        string `validate:\"required,gte=8,lte=32\" json:\"pass\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n\tIP          string `json:\"-\" `\n}\n\nfunc (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tif err = checker.CheckPassword(u.Pass); err != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"pass\",\n\t\t\tErrorMsg:   err.Error(),\n\t\t})\n\t\treturn errFields, err\n\t}\n\treturn nil, nil\n}\n\ntype UserModifyPasswordReq struct {\n\tOldPass     string `validate:\"omitempty,gte=8,lte=32\" json:\"old_pass\"`\n\tPass        string `validate:\"required,gte=8,lte=32\" json:\"pass\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n\tUserID      string `json:\"-\"`\n\tAccessToken string `json:\"-\"`\n}\n\nfunc (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {\n\tif err = checker.CheckPassword(u.Pass); err != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"pass\",\n\t\t\tErrorMsg:   err.Error(),\n\t\t})\n\t\treturn errFields, err\n\t}\n\treturn nil, nil\n}\n\ntype UpdateInfoRequest struct {\n\tDisplayName string     `validate:\"omitempty,gte=2,lte=30\" json:\"display_name\"`\n\tUsername    string     `validate:\"omitempty,gte=2,lte=30\" json:\"username\"`\n\tAvatar      AvatarInfo `json:\"avatar\"`\n\tBio         string     `validate:\"omitempty,gt=0,lte=4096\" json:\"bio\"`\n\tBioHTML     string     `json:\"-\"`\n\tWebsite     string     `validate:\"omitempty,gt=0,lte=500\" json:\"website\"`\n\tLocation    string     `validate:\"omitempty,gt=0,lte=100\" json:\"location\"`\n\tUserID      string     `json:\"-\"`\n\tIsAdmin     bool       `json:\"-\"`\n}\n\ntype AvatarInfo struct {\n\tType     string `validate:\"omitempty,gt=0,lte=100\"  json:\"type\"`\n\tGravatar string `validate:\"omitempty,gt=0,lte=200\"  json:\"gravatar\"`\n\tCustom   string `validate:\"omitempty,gt=0,lte=200\"  json:\"custom\"`\n}\n\nfunc (a *AvatarInfo) ToJsonString() string {\n\tdata, _ := json.Marshal(a)\n\treturn string(data)\n}\n\nfunc (a *AvatarInfo) GetURL() string {\n\tswitch a.Type {\n\tcase constant.AvatarTypeGravatar:\n\t\treturn a.Gravatar\n\tcase constant.AvatarTypeCustom:\n\t\treturn a.Custom\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc CustomAvatar(url string) *AvatarInfo {\n\treturn &AvatarInfo{\n\t\tType:   constant.AvatarTypeCustom,\n\t\tCustom: url,\n\t}\n}\n\nfunc (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {\n\treq.BioHTML = converter.Markdown2BasicHTML(req.Bio)\n\tif len(req.Website) > 0 && !checker.IsURL(req.Website) {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"website\",\n\t\t\tErrorMsg:   reason.InvalidURLError,\n\t\t}), errors.BadRequest(reason.InvalidURLError)\n\t}\n\treturn nil, nil\n}\n\n// UpdateUserInterfaceRequest update user interface request\ntype UpdateUserInterfaceRequest struct {\n\t// language\n\tLanguage string `validate:\"required,gt=1,lte=100\" json:\"language\"`\n\t// Color scheme\n\tColorScheme string `validate:\"required,gt=1,lte=100\" json:\"color_scheme\"`\n\t// user id\n\tUserId string `json:\"-\"`\n}\n\nfunc (req *UpdateUserInterfaceRequest) Check() (errFields []*validator.FormErrorField, err error) {\n\tif !translator.CheckLanguageIsValid(req.Language) {\n\t\treturn nil, errors.BadRequest(reason.LangNotFound)\n\t}\n\tif req.ColorScheme != constant.ColorSchemeDefault &&\n\t\treq.ColorScheme != constant.ColorSchemeLight &&\n\t\treq.ColorScheme != constant.ColorSchemeDark &&\n\t\treq.ColorScheme != constant.ColorSchemeSystem {\n\t\treq.ColorScheme = constant.ColorSchemeDefault\n\t}\n\treturn nil, nil\n}\n\ntype UserRetrievePassWordRequest struct {\n\tEmail       string `validate:\"required,email,gt=0,lte=500\" json:\"e_mail\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\ntype UserRePassWordRequest struct {\n\tCode    string `validate:\"required,gt=0,lte=100\" json:\"code\"`\n\tPass    string `validate:\"required,gt=0,lte=32\" json:\"pass\"`\n\tContent string `json:\"-\"`\n}\n\nfunc (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {\n\tif err = checker.CheckPassword(u.Pass); err != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"pass\",\n\t\t\tErrorMsg:   err.Error(),\n\t\t})\n\t\treturn errFields, err\n\t}\n\treturn nil, nil\n}\n\ntype ActionRecordReq struct {\n\tAction string `validate:\"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote\" form:\"action\"`\n\tIP     string `json:\"-\"`\n\tUserID string `json:\"-\"`\n}\n\ntype ActionRecordResp struct {\n\tCaptchaID  string `json:\"captcha_id\"`\n\tCaptchaImg string `json:\"captcha_img\"`\n\tVerify     bool   `json:\"verify\"`\n}\n\ntype UserBasicInfo struct {\n\tID             string `json:\"id\"`\n\tUsername       string `json:\"username\"`\n\tRank           int    `json:\"rank\"`\n\tDisplayName    string `json:\"display_name\"`\n\tAvatar         string `json:\"avatar\"`\n\tWebsite        string `json:\"website\"`\n\tLocation       string `json:\"location\"`\n\tLanguage       string `json:\"language\"`\n\tStatus         string `json:\"status\"`\n\tSuspendedUntil int64  `json:\"suspended_until\"`\n}\n\ntype GetOtherUserInfoByUsernameReq struct {\n\tUsername string `validate:\"required,gt=0,lte=500\" form:\"username\"`\n\tUserID   string `json:\"-\"`\n\tIsAdmin  bool   `json:\"-\"`\n}\n\ntype GetOtherUserInfoResp struct {\n\tInfo *GetOtherUserInfoByUsernameResp `json:\"info\"`\n}\n\ntype UserChangeEmailSendCodeReq struct {\n\tUserVerifyEmailSendReq\n\tEmail  string `validate:\"required,email,gt=0,lte=500\" json:\"e_mail\"`\n\tPass   string `validate:\"omitempty,gte=8,lte=32\" json:\"pass\"`\n\tUserID string `json:\"-\"`\n}\n\ntype UserChangeEmailVerifyReq struct {\n\tCode    string `validate:\"required,gt=0,lte=500\" json:\"code\"`\n\tContent string `json:\"-\"`\n}\n\ntype UserVerifyEmailSendReq struct {\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n}\n\n// UserRankingResp user ranking response\ntype UserRankingResp struct {\n\tUsersWithTheMostReputation []*UserRankingSimpleInfo `json:\"users_with_the_most_reputation\"`\n\tUsersWithTheMostVote       []*UserRankingSimpleInfo `json:\"users_with_the_most_vote\"`\n\tStaffs                     []*UserRankingSimpleInfo `json:\"staffs\"`\n}\n\n// UserRankingSimpleInfo user ranking simple info\ntype UserRankingSimpleInfo struct {\n\t// username\n\tUsername string `json:\"username\"`\n\t// rank\n\tRank int `json:\"rank\"`\n\t// vote\n\tVoteCount int `json:\"vote_count\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// avatar\n\tAvatar string `json:\"avatar\"`\n}\n\n// UserUnsubscribeNotificationReq user unsubscribe email notification request\ntype UserUnsubscribeNotificationReq struct {\n\tCode    string `validate:\"required,gt=0,lte=500\" json:\"code\"`\n\tContent string `json:\"-\"`\n}\n\n// GetUserStaffReq get user staff request\ntype GetUserStaffReq struct {\n\tUsername string `validate:\"omitempty,gt=0,lte=500\" form:\"username\"`\n\tPageSize int    `validate:\"omitempty,min=1\" form:\"page_size\"`\n}\n\n// GetUserStaffResp get user staff response\ntype GetUserStaffResp struct {\n\t// username\n\tUsername string `json:\"username\"`\n\t// display name\n\tDisplayName string `json:\"display_name\"`\n\t// avatar\n\tAvatar string `json:\"avatar\"`\n}\n"
  },
  {
    "path": "internal/schema/vote_schema.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage schema\n\ntype VoteReq struct {\n\tObjectID    string `validate:\"required\" json:\"object_id\"`\n\tIsCancel    bool   `validate:\"omitempty\" json:\"is_cancel\"`\n\tCaptchaID   string `json:\"captcha_id\"`\n\tCaptchaCode string `json:\"captcha_code\"`\n\tUserID      string `json:\"-\"`\n}\n\ntype VoteResp struct {\n\tUpVotes    int64  `json:\"up_votes\"`\n\tDownVotes  int64  `json:\"down_votes\"`\n\tVotes      int64  `json:\"votes\"`\n\tVoteStatus string `json:\"vote_status\"`\n}\n\n// VoteOperationInfo vote operation info\ntype VoteOperationInfo struct {\n\t// operation object id\n\tObjectID string\n\t// question answer comment\n\tObjectType string\n\t// object owner user id\n\tObjectCreatorUserID string\n\t// operation user id\n\tOperatingUserID string\n\t// vote up\n\tVoteUp bool\n\t// vote down\n\tVoteDown bool\n\t// vote activity info\n\tActivities []*VoteActivity\n}\n\n// VoteActivity vote activity\ntype VoteActivity struct {\n\tActivityType   int\n\tActivityUserID string\n\tTriggerUserID  string\n\tRank           int\n}\n\nfunc (v *VoteActivity) HasRank() int {\n\tif v.Rank != 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\ntype GetVoteWithPageReq struct {\n\t// page\n\tPage int `validate:\"omitempty,min=1\" form:\"page\"`\n\t// page size\n\tPageSize int `validate:\"omitempty,min=1\" form:\"page_size\"`\n\t// user id\n\tUserID string `json:\"-\"`\n}\n\ntype GetVoteWithPageResp struct {\n\t// create time\n\tCreatedAt int64 `json:\"created_at\"`\n\t// object id\n\tObjectID string `json:\"object_id\"`\n\t// question id\n\tQuestionID string `json:\"question_id\"`\n\t// answer id\n\tAnswerID string `json:\"answer_id\"`\n\t// object type\n\tObjectType string `json:\"object_type\" enums:\"question,answer,tag,comment\"`\n\t// title\n\tTitle string `json:\"title\"`\n\t// url title\n\tUrlTitle string `json:\"url_title\"`\n\t// content\n\tContent string `json:\"content\"`\n\t// vote type\n\tVoteType string `json:\"vote_type\"`\n}\n"
  },
  {
    "path": "internal/service/action/captcha_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage action\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// CaptchaRepo captcha repository\ntype CaptchaRepo interface {\n\tSetCaptcha(ctx context.Context, key, captcha string) (err error)\n\tGetCaptcha(ctx context.Context, key string) (captcha string, err error)\n\tDelCaptcha(ctx context.Context, key string) (err error)\n\tSetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error)\n\tGetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error)\n\tDelActionType(ctx context.Context, unit, actionType string) (err error)\n}\n\n// CaptchaService kit service\ntype CaptchaService struct {\n\tcaptchaRepo CaptchaRepo\n}\n\n// NewCaptchaService captcha service\nfunc NewCaptchaService(captchaRepo CaptchaRepo) *CaptchaService {\n\treturn &CaptchaService{\n\t\tcaptchaRepo: captchaRepo,\n\t}\n}\n\n// ActionRecord action record\nfunc (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRecordReq) (resp *schema.ActionRecordResp, err error) {\n\tresp = &schema.ActionRecordResp{}\n\tunit := req.IP\n\tswitch req.Action {\n\tcase entity.CaptchaActionEditUserinfo:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionQuestion:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionAnswer:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionComment:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionEdit:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionInvitationAnswer:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionSearch:\n\t\tif req.UserID != \"\" {\n\t\t\tunit = req.UserID\n\t\t}\n\tcase entity.CaptchaActionReport:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionDelete:\n\t\tunit = req.UserID\n\tcase entity.CaptchaActionVote:\n\t\tunit = req.UserID\n\t}\n\tverificationResult := cs.ValidationStrategy(ctx, unit, req.Action)\n\tif !verificationResult {\n\t\tresp.Verify = true\n\t\tresp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"GenerateCaptcha error: %v\", err)\n\t\t}\n\t}\n\treturn\n}\n\n// ActionRecordVerifyCaptcha\n// Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct\nfunc (cs *CaptchaService) ActionRecordVerifyCaptcha(\n\tctx context.Context, actionType string, unit string, captchaID string, captchaCode string,\n) bool {\n\tverificationResult := cs.ValidationStrategy(ctx, unit, actionType)\n\tif verificationResult {\n\t\treturn true\n\t}\n\tpass, err := cs.VerifyCaptcha(ctx, captchaID, captchaCode)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn pass\n}\n\nfunc (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, unit string) {\n\tinfo, err := cs.captchaRepo.GetActionType(ctx, unit, actionType)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tamount := 1\n\tif info != nil {\n\t\tamount = info.Num + 1\n\t}\n\terr = cs.captchaRepo.SetActionType(ctx, unit, actionType, \"\", amount)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n}\n\nfunc (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, unit string) {\n\terr := cs.captchaRepo.DelActionType(ctx, unit, actionType)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n}\n\n// GenerateCaptcha generate captcha\nfunc (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase64 string, err error) {\n\trealCaptcha := \"\"\n\tkey = token.GenerateToken()\n\t_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {\n\t\tif captcha, code := fn.Create(); len(code) > 0 {\n\t\t\tcaptchaBase64 = captcha\n\t\t\trealCaptcha = code\n\t\t}\n\t\treturn nil\n\t})\n\tif len(realCaptcha) == 0 {\n\t\treturn key, captchaBase64, nil\n\t}\n\n\terr = cs.captchaRepo.SetCaptcha(ctx, key, realCaptcha)\n\treturn key, captchaBase64, err\n}\n\n// VerifyCaptcha generate captcha\nfunc (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) {\n\trealCaptcha, _ := cs.captchaRepo.GetCaptcha(ctx, key)\n\n\t_ = plugin.CallCaptcha(func(fn plugin.Captcha) error {\n\t\tisCorrect = fn.Verify(realCaptcha, captcha)\n\t\treturn nil\n\t})\n\n\t_ = cs.captchaRepo.DelCaptcha(ctx, key)\n\treturn isCorrect, nil\n}\n"
  },
  {
    "path": "internal/service/action/captcha_strategy.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage action\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// ValidationStrategy\n// true pass\n// false need captcha\nfunc (cs *CaptchaService) ValidationStrategy(ctx context.Context, unit, actionType string) bool {\n\t// If the captcha is not enabled, the verification is passed directly\n\tif !plugin.CaptchaEnabled() {\n\t\treturn true\n\t}\n\tinfo, err := cs.captchaRepo.GetActionType(ctx, unit, actionType)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false\n\t}\n\tswitch actionType {\n\tcase entity.CaptchaActionEmail:\n\t\treturn cs.CaptchaActionEmail(ctx, unit, info)\n\tcase entity.CaptchaActionPassword:\n\t\treturn cs.CaptchaActionPassword(ctx, unit, info)\n\tcase entity.CaptchaActionEditUserinfo:\n\t\treturn cs.CaptchaActionEditUserinfo(ctx, unit, info)\n\tcase entity.CaptchaActionQuestion:\n\t\treturn cs.CaptchaActionQuestion(ctx, unit, info)\n\tcase entity.CaptchaActionAnswer:\n\t\treturn cs.CaptchaActionAnswer(ctx, unit, info)\n\tcase entity.CaptchaActionComment:\n\t\treturn cs.CaptchaActionComment(ctx, unit, info)\n\tcase entity.CaptchaActionEdit:\n\t\treturn cs.CaptchaActionEdit(ctx, unit, info)\n\tcase entity.CaptchaActionInvitationAnswer:\n\t\treturn cs.CaptchaActionInvitationAnswer(ctx, unit, info)\n\tcase entity.CaptchaActionSearch:\n\t\treturn cs.CaptchaActionSearch(ctx, unit, info)\n\tcase entity.CaptchaActionReport:\n\t\treturn cs.CaptchaActionReport(ctx, unit, info)\n\tcase entity.CaptchaActionDelete:\n\t\treturn cs.CaptchaActionDelete(ctx, unit, info)\n\tcase entity.CaptchaActionVote:\n\t\treturn cs.CaptchaActionVote(ctx, unit, info)\n\t}\n\t// actionType not found\n\treturn false\n}\n\nfunc (cs *CaptchaService) CaptchaActionEmail(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\t// You need a verification code every time\n\treturn false\n}\n\nfunc (cs *CaptchaService) CaptchaActionPassword(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 3\n\tsetTime := int64(60 * 30) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\tif now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime {\n\t\tif err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionPassword, \"\", 0); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionEditUserinfo(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 3\n\tsetTime := int64(60 * 30) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\tif now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime {\n\t\tif err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionEditUserinfo, \"\", 0); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionQuestion(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 10\n\tsetTime := int64(5) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 10\n\tsetTime := int64(5) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionComment(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 30\n\tsetTime := int64(1) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionEdit(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 10\n\treturn actionInfo.Num < setNum\n}\n\nfunc (cs *CaptchaService) CaptchaActionInvitationAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 30\n\treturn actionInfo.Num < setNum\n}\n\nfunc (cs *CaptchaService) CaptchaActionSearch(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tnow := time.Now().Unix()\n\tsetNum := 20\n\tsetTime := int64(60) // seconds\n\tif now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\tif now-actionInfo.LastTime > setTime {\n\t\tif err := cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionSearch, \"\", 0); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionReport(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 30\n\tsetTime := int64(1) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionDelete(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 5\n\tsetTime := int64(5) // seconds\n\tnow := time.Now().Unix()\n\tif now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (cs *CaptchaService) CaptchaActionVote(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool {\n\tif actionInfo == nil {\n\t\treturn true\n\t}\n\tsetNum := 40\n\treturn actionInfo.Num < setNum\n}\n"
  },
  {
    "path": "internal/service/activity/activity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// ActivityRepo activity repository\ntype ActivityRepo interface {\n\tGetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error)\n}\n\n// ActivityService activity service\ntype ActivityService struct {\n\tactivityRepo          ActivityRepo\n\tuserCommon            *usercommon.UserCommon\n\tactivityCommonService *activity_common.ActivityCommon\n\ttagCommonService      *tag_common.TagCommonService\n\tobjectInfoService     *object_info.ObjService\n\tcommentCommonService  *comment_common.CommentCommonService\n\trevisionService       *revision_common.RevisionService\n\tmetaService           *metacommon.MetaCommonService\n\tconfigService         *config.ConfigService\n}\n\n// NewActivityService new activity service\nfunc NewActivityService(\n\tactivityRepo ActivityRepo,\n\tuserCommon *usercommon.UserCommon,\n\tactivityCommonService *activity_common.ActivityCommon,\n\ttagCommonService *tag_common.TagCommonService,\n\tobjectInfoService *object_info.ObjService,\n\tcommentCommonService *comment_common.CommentCommonService,\n\trevisionService *revision_common.RevisionService,\n\tmetaService *metacommon.MetaCommonService,\n\tconfigService *config.ConfigService,\n) *ActivityService {\n\treturn &ActivityService{\n\t\tobjectInfoService:     objectInfoService,\n\t\tactivityRepo:          activityRepo,\n\t\tuserCommon:            userCommon,\n\t\tactivityCommonService: activityCommonService,\n\t\ttagCommonService:      tagCommonService,\n\t\tcommentCommonService:  commentCommonService,\n\t\trevisionService:       revisionService,\n\t\tmetaService:           metaService,\n\t\tconfigService:         configService,\n\t}\n}\n\n// GetObjectTimeline get object timeline\nfunc (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) (\n\tresp *schema.GetObjectTimelineResp, err error) {\n\tresp = &schema.GetObjectTimelineResp{\n\t\tObjectInfo: &schema.ActObjectInfo{},\n\t\tTimeline:   make([]*schema.ActObjectTimeline, 0),\n\t}\n\n\tresp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tactivityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, act := range activityList {\n\t\titem := &schema.ActObjectTimeline{\n\t\t\tActivityID: act.ID,\n\t\t\tRevisionID: converter.IntToString(act.RevisionID),\n\t\t\tCreatedAt:  act.CreatedAt.Unix(),\n\t\t\tCancelled:  act.Cancelled == entity.ActivityCancelled,\n\t\t\tObjectID:   act.ObjectID,\n\t\t\tUserInfo:   &schema.UserBasicInfo{},\n\t\t}\n\t\titem.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID)\n\t\tif item.Cancelled {\n\t\t\titem.CancelledAt = act.CancelledAt.Unix()\n\t\t}\n\n\t\tif item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType {\n\t\t\tif handler.GetEnableShortID(ctx) {\n\t\t\t\titem.ObjectID = uid.EnShortID(act.ObjectID)\n\t\t\t}\n\t\t}\n\n\t\tcfg, err := as.configService.GetConfigByID(ctx, act.ActivityType)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"fail to get config by id: %d, err: %v, act id is: %s\", act.ActivityType, err, act.ID)\n\t\t} else {\n\t\t\t// database save activity type is number, change to activity type string is like \"question.asked\".\n\t\t\t// so we need to cut the front part of '.', only need string like 'asked'\n\t\t\t_, item.ActivityType, _ = strings.Cut(cfg.Key, \".\")\n\t\t\t// format activity type string to show\n\t\t\tif isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\titem.ActivityType = formattedActivityType\n\t\t\t}\n\t\t}\n\n\t\t// if activity is down vote, only admin can see who does it.\n\t\tif item.ActivityType == constant.ActDownVote && !req.IsAdmin {\n\t\t\titem.UserInfo.Username = \"N/A\"\n\t\t\titem.UserInfo.DisplayName = \"N/A\"\n\t\t} else {\n\t\t\tif act.TriggerUserID > 0 {\n\t\t\t\titem.UserInfo.ID = fmt.Sprintf(\"%d\", act.TriggerUserID)\n\t\t\t} else {\n\t\t\t\titem.UserInfo.ID = act.UserID\n\t\t\t}\n\t\t}\n\n\t\titem.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)\n\t\tresp.Timeline = append(resp.Timeline, item)\n\t}\n\tas.formatTimelineUserInfo(ctx, resp.Timeline)\n\treturn\n}\n\nfunc (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) (\n\tresp *schema.ActObjectInfo, err error) {\n\tresp = &schema.ActObjectInfo{}\n\tobjInfo, err := as.objectInfoService.GetInfo(ctx, objectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Title = objInfo.Title\n\tif objInfo.ObjectType == constant.TagObjectType {\n\t\ttag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID)\n\t\tif exist {\n\t\t\tresp.Title = tag.SlugName\n\t\t\tresp.MainTagSlugName = tag.MainTagSlugName\n\t\t}\n\t}\n\tresp.ObjectType = objInfo.ObjectType\n\tresp.QuestionID = objInfo.QuestionID\n\tresp.AnswerID = objInfo.AnswerID\n\tif len(objInfo.ObjectCreatorUserID) > 0 {\n\t\t// get object creator user info\n\t\tuserBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tresp.Username = userBasicInfo.Username\n\t\t\tresp.DisplayName = userBasicInfo.DisplayName\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType,\n\tactivityType, revisionID string) (comment string) {\n\tif objectType == constant.CommentObjectType {\n\t\tcommentInfo, err := as.commentCommonService.GetComment(ctx, objectID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\treturn commentInfo.ParsedText\n\t\t}\n\t\treturn\n\t}\n\n\tif activityType == constant.ActEdited {\n\t\trevision, err := as.revisionService.GetRevision(ctx, revisionID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\treturn converter.Markdown2HTML(revision.Log)\n\t\t}\n\t\treturn\n\t}\n\tif activityType == constant.ActClosed {\n\t\t// only question can be closed\n\t\tmetaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\tcloseMsg := &schema.CloseQuestionMeta{}\n\t\t\tif err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil {\n\t\t\t\treturn converter.Markdown2HTML(closeMsg.CloseMsg)\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) {\n\tuserExist := make(map[string]bool)\n\tuserIDs := make([]string, 0)\n\tfor _, info := range timeline {\n\t\tif len(info.UserInfo.ID) == 0 || userExist[info.UserInfo.ID] {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, info.UserInfo.ID)\n\t}\n\tif len(userIDs) == 0 {\n\t\treturn\n\t}\n\tuserInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tfor _, info := range timeline {\n\t\tif len(info.UserInfo.ID) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tinfo.UserInfo = userInfoMapping[info.UserInfo.ID]\n\t}\n}\n\n// GetObjectTimelineDetail get object timeline\nfunc (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) (\n\tresp *schema.GetObjectTimelineDetailResp, err error) {\n\tresp = &schema.GetObjectTimelineDetailResp{}\n\tresp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID)\n\tresp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID)\n\treturn resp, nil\n}\n\n// getOneObjectDetail get object detail\nfunc (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) (\n\tresp *schema.ObjectTimelineDetail, err error) {\n\tresp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)}\n\n\t// if request revision is 0, return null object detail.\n\tif revisionID == \"0\" {\n\t\treturn nil, nil\n\t}\n\n\trevision, err := as.revisionService.GetRevision(ctx, revisionID)\n\tif err != nil {\n\t\tlog.Warn(err)\n\t\treturn nil, nil\n\t}\n\tobjInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch objInfo.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tdata := &entity.QuestionWithTagsRevision{}\n\t\tif err = json.Unmarshal([]byte(revision.Content), data); err != nil {\n\t\t\tlog.Errorf(\"revision parsing error %s\", err)\n\t\t\treturn resp, nil\n\t\t}\n\t\tfor _, tag := range data.Tags {\n\t\t\tresp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{\n\t\t\t\tSlugName:        tag.SlugName,\n\t\t\t\tDisplayName:     tag.DisplayName,\n\t\t\t\tMainTagSlugName: tag.MainTagSlugName,\n\t\t\t\tRecommend:       tag.Recommend,\n\t\t\t\tReserved:        tag.Reserved,\n\t\t\t})\n\t\t}\n\t\tresp.Title = data.Title\n\t\tresp.OriginalText = data.OriginalText\n\tcase constant.AnswerObjectType:\n\t\tdata := &entity.Answer{}\n\t\tif err = json.Unmarshal([]byte(revision.Content), data); err != nil {\n\t\t\tlog.Errorf(\"revision parsing error %s\", err)\n\t\t\treturn resp, nil\n\t\t}\n\t\tresp.Title = objInfo.Title // answer show question title\n\t\tresp.OriginalText = data.OriginalText\n\tcase constant.TagObjectType:\n\t\tdata := &entity.Tag{}\n\t\tif err = json.Unmarshal([]byte(revision.Content), data); err != nil {\n\t\t\tlog.Errorf(\"revision parsing error %s\", err)\n\t\t\treturn resp, nil\n\t\t}\n\t\tresp.Title = data.DisplayName\n\t\tresp.OriginalText = data.OriginalText\n\t\tresp.SlugName = data.SlugName\n\t\tresp.MainTagSlugName = data.MainTagSlugName\n\tdefault:\n\t\tlog.Errorf(\"unknown object type %s\", objInfo.ObjectType)\n\t}\n\treturn resp, nil\n}\n\nfunc formatActivity(activityType string) (isHidden bool, formattedActivityType string) {\n\tif activityType == constant.ActVotedUp ||\n\t\tactivityType == constant.ActVotedDown ||\n\t\tactivityType == constant.ActFollow {\n\t\treturn true, \"\"\n\t}\n\tif activityType == constant.ActVoteUp {\n\t\treturn false, constant.ActUpVote\n\t}\n\tif activityType == constant.ActVoteDown {\n\t\treturn false, constant.ActDownVote\n\t}\n\tif activityType == constant.ActAccepted {\n\t\treturn false, constant.ActAccept\n\t}\n\treturn false, activityType\n}\n"
  },
  {
    "path": "internal/service/activity/answer_activity_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_type\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// AnswerActivityRepo answer activity\ntype AnswerActivityRepo interface {\n\tSaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error)\n\tSaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error)\n}\n\n// AnswerActivityService answer activity service\ntype AnswerActivityService struct {\n\tanswerActivityRepo AnswerActivityRepo\n\tconfigService      *config.ConfigService\n}\n\n// NewAnswerActivityService new comment service\nfunc NewAnswerActivityService(\n\tanswerActivityRepo AnswerActivityRepo,\n\tconfigService *config.ConfigService,\n) *AnswerActivityService {\n\treturn &AnswerActivityService{\n\t\tanswerActivityRepo: answerActivityRepo,\n\t\tconfigService:      configService,\n\t}\n}\n\n// AcceptAnswer accept answer change activity\nfunc (as *AnswerActivityService) AcceptAnswer(ctx context.Context,\n\tloginUserID, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) {\n\tlog.Debugf(\"user %s want to accept answer %s[%s] for question %s[%s]\", loginUserID,\n\t\tanswerObjID, answerUserID,\n\t\tquestionObjID, questionUserID)\n\toperationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID,\n\t\tanswerObjID, questionObjID, questionUserID, answerUserID, isSelf)\n\treturn as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo)\n}\n\n// CancelAcceptAnswer cancel accept answer change activity\nfunc (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,\n\tloginUserID, answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {\n\toperationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID,\n\t\tanswerObjID, questionObjID, questionUserID, answerUserID, false)\n\treturn as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo)\n}\n\nfunc (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context, loginUserID,\n\tanswerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo {\n\toperationInfo := &schema.AcceptAnswerOperationInfo{\n\t\tTriggerUserID:    loginUserID,\n\t\tQuestionObjectID: questionObjID,\n\t\tQuestionUserID:   questionUserID,\n\t\tAnswerObjectID:   answerObjID,\n\t\tAnswerUserID:     answerUserID,\n\t}\n\toperationInfo.Activities = as.getActivities(ctx, operationInfo)\n\tif isSelf {\n\t\tfor _, activity := range operationInfo.Activities {\n\t\t\tactivity.Rank = 0\n\t\t}\n\t}\n\treturn operationInfo\n}\n\nfunc (as *AnswerActivityService) getActivities(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (\n\tactivities []*schema.AcceptAnswerActivity) {\n\tactivities = make([]*schema.AcceptAnswerActivity, 0)\n\n\tfor _, action := range []string{activity_type.AnswerAccept, activity_type.AnswerAccepted} {\n\t\tt := &schema.AcceptAnswerActivity{}\n\t\tcfg, err := as.configService.GetConfigByKey(ctx, action)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"get config by key error: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tt.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()\n\n\t\tif action == activity_type.AnswerAccept {\n\t\t\tt.ActivityUserID = op.QuestionUserID\n\t\t\tt.TriggerUserID = op.TriggerUserID\n\t\t\tt.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer.\n\t\t} else {\n\t\t\tt.ActivityUserID = op.AnswerUserID\n\t\t\tt.TriggerUserID = op.TriggerUserID\n\t\t\tt.OriginalObjectID = op.AnswerObjectID // if activity is 'accepted' means this answer was accepted.\n\t\t}\n\t\tactivities = append(activities, t)\n\t}\n\treturn activities\n}\n"
  },
  {
    "path": "internal/service/activity/review_active.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// ReviewActivityRepo interface\ntype ReviewActivityRepo interface {\n\tReview(ctx context.Context, sct *schema.PassReviewActivity) (err error)\n}\n"
  },
  {
    "path": "internal/service/activity/user_active.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity\n\nimport \"context\"\n\n// UserActiveActivityRepo interface\ntype UserActiveActivityRepo interface {\n\tUserActive(ctx context.Context, userID string) (err error)\n}\n"
  },
  {
    "path": "internal/service/activity_common/activity.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\ntype ActivityRepo interface {\n\tGetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error)\n\tGetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error)\n\tGetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (\n\t\texistsActivity *entity.Activity, exist bool, err error)\n\tGetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) (activityList []*entity.Activity, err error)\n\tGetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)\n\tGetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error)\n\tAddActivity(ctx context.Context, activity *entity.Activity) (err error)\n\tGetUsersWhoHasGainedTheMostReputation(\n\t\tctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error)\n\tGetUsersWhoHasVoteMost(\n\t\tctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error)\n}\n\ntype ActivityCommon struct {\n\tactivityRepo         ActivityRepo\n\tactivityQueueService activityqueue.Service\n}\n\n// NewActivityCommon new activity common\nfunc NewActivityCommon(\n\tactivityRepo ActivityRepo,\n\tactivityQueueService activityqueue.Service,\n) *ActivityCommon {\n\tactivity := &ActivityCommon{\n\t\tactivityRepo:         activityRepo,\n\t\tactivityQueueService: activityQueueService,\n\t}\n\tactivity.activityQueueService.RegisterHandler(activity.HandleActivity)\n\treturn activity\n}\n\n// HandleActivity handle activity message\nfunc (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error {\n\tactivityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey))\n\tif err != nil {\n\t\tlog.Errorf(\"error getting activity type %s, activity type is %d\", err, activityType)\n\t\treturn err\n\t}\n\n\tact := &entity.Activity{\n\t\tUserID:           msg.UserID,\n\t\tTriggerUserID:    msg.TriggerUserID,\n\t\tObjectID:         uid.DeShortID(msg.ObjectID),\n\t\tOriginalObjectID: uid.DeShortID(msg.OriginalObjectID),\n\t\tActivityType:     activityType,\n\t\tCancelled:        entity.ActivityAvailable,\n\t}\n\tif len(msg.RevisionID) > 0 {\n\t\tact.RevisionID = converter.StringToInt64(msg.RevisionID)\n\t}\n\tif err := ac.activityRepo.AddActivity(ctx, act); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/activity_common/follow.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport \"context\"\n\ntype FollowRepo interface {\n\tGetFollowIDs(ctx context.Context, userID, objectType string) (followIDs []string, err error)\n\tGetFollowAmount(ctx context.Context, objectID string) (followAmount int, err error)\n\tGetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error)\n\tIsFollowed(ctx context.Context, userId, objectId string) (bool, error)\n\tMigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error\n}\n"
  },
  {
    "path": "internal/service/activity_common/vote.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_common\n\nimport (\n\t\"context\"\n)\n\n// VoteRepo activity repository\ntype VoteRepo interface {\n\tGetVoteStatus(ctx context.Context, objectId, userId string) (status string)\n\tGetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error)\n}\n"
  },
  {
    "path": "internal/service/activity_type/activity_type.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activity_type\n\nconst (\n\tQuestionVoteUp    = \"question.vote_up\"\n\tQuestionVoteDown  = \"question.vote_down\"\n\tQuestionVotedUp   = \"question.voted_up\"\n\tQuestionVotedDown = \"question.voted_down\"\n\tAnswerVoteUp      = \"answer.vote_up\"\n\tAnswerVoteDown    = \"answer.vote_down\"\n\tAnswerVotedUp     = \"answer.voted_up\"\n\tAnswerVotedDown   = \"answer.voted_down\"\n\tAnswerAccepted    = \"answer.accepted\"\n\tAnswerAccept      = \"answer.accept\"\n\tCommentVoteUp     = \"comment.vote_up\"\n\tEditAccepted      = \"edit.accepted\"\n)\n\nvar (\n\tActivityTypeList = []string{\n\t\tQuestionVoteUp,\n\t\tQuestionVoteDown,\n\t\tQuestionVotedUp,\n\t\tQuestionVotedDown,\n\t\tAnswerVoteUp,\n\t\tAnswerVoteDown,\n\t\tAnswerVotedUp,\n\t\tAnswerVotedDown,\n\t\tAnswerAccepted,\n\t\tAnswerAccept,\n\t\tCommentVoteUp,\n\t}\n\tVoteActivityTypeList = []string{\n\t\tQuestionVoteUp,\n\t\tQuestionVoteDown,\n\t\tQuestionVotedUp,\n\t\tQuestionVotedDown,\n\t\tAnswerVoteUp,\n\t\tAnswerVoteDown,\n\t\tAnswerVotedUp,\n\t\tAnswerVotedDown,\n\t\tCommentVoteUp,\n\t}\n\tActivityTypeFlagMapping = map[string]string{\n\t\tQuestionVoteUp:    \"action_activity_type.upvote\",\n\t\tQuestionVoteDown:  \"action_activity_type.downvote\",\n\t\tQuestionVotedUp:   \"action_activity_type.upvoted\",\n\t\tQuestionVotedDown: \"action_activity_type.downvoted\",\n\t\tAnswerVoteUp:      \"action_activity_type.upvote\",\n\t\tAnswerVoteDown:    \"action_activity_type.downvote\",\n\t\tAnswerVotedUp:     \"action_activity_type.upvoted\",\n\t\tAnswerVotedDown:   \"action_activity_type.downvoted\",\n\t\tAnswerAccepted:    \"action_activity_type.accepted\",\n\t\tAnswerAccept:      \"action_activity_type.accept\",\n\t\tCommentVoteUp:     \"action_activity_type.upvote\",\n\t\tEditAccepted:      \"action_activity_type.edit\",\n\t}\n)\n"
  },
  {
    "path": "internal/service/activityqueue/activity_queue.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage activityqueue\n\nimport (\n\t\"github.com/apache/answer/internal/base/queue\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\ntype Service queue.Service[*schema.ActivityMsg]\n\nfunc NewService() Service {\n\treturn queue.New[*schema.ActivityMsg](\"activity\", 128)\n}\n"
  },
  {
    "path": "internal/service/ai_conversation/ai_conversation_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage ai_conversation\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/repo/ai_conversation\"\n\t\"github.com/apache/answer/internal/schema\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// AIConversationService\ntype AIConversationService interface {\n\tCreateConversation(ctx context.Context, userID, conversationID, topic string) error\n\tSaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error\n\tGetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error)\n\tGetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) (resp *schema.AIConversationDetailResp, exist bool, err error)\n\tVoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error\n\tGetConversationListForAdmin(ctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error)\n\tGetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error)\n\tDeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error\n}\n\n// ConversationMessage\ntype ConversationMessage struct {\n\tChatCompletionID string `json:\"chat_completion_id\"`\n\tRole             string `json:\"role\"`\n\tContent          string `json:\"content\"`\n}\n\n// aiConversationService\ntype aiConversationService struct {\n\taiConversationRepo ai_conversation.AIConversationRepo\n\tuserCommon         *usercommon.UserCommon\n}\n\n// NewAIConversationService\nfunc NewAIConversationService(\n\taiConversationRepo ai_conversation.AIConversationRepo,\n\tuserCommon *usercommon.UserCommon,\n) AIConversationService {\n\treturn &aiConversationService{\n\t\taiConversationRepo: aiConversationRepo,\n\t\tuserCommon:         userCommon,\n\t}\n}\n\n// CreateConversation\nfunc (s *aiConversationService) CreateConversation(ctx context.Context, userID, conversationID, topic string) error {\n\tconversation := &entity.AIConversation{\n\t\tConversationID: conversationID,\n\t\tTopic:          topic,\n\t\tUserID:         userID,\n\t}\n\terr := s.aiConversationRepo.CreateConversation(ctx, conversation)\n\tif err != nil {\n\t\tlog.Errorf(\"create conversation failed: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// SaveConversationRecords\nfunc (s *aiConversationService) SaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error {\n\tconversation, exist, err := s.aiConversationRepo.GetConversation(ctx, conversationID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tcontent := strings.Builder{}\n\n\tfor _, record := range records {\n\t\tif len(record.ChatCompletionID) > 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif record.Role == \"user\" {\n\t\t\taiRecord := &entity.AIConversationRecord{\n\t\t\t\tConversationID:   conversationID,\n\t\t\t\tChatCompletionID: chatcmplID,\n\t\t\t\tRole:             \"user\",\n\t\t\t\tContent:          record.Content,\n\t\t\t}\n\n\t\t\terr = s.aiConversationRepo.CreateRecord(ctx, aiRecord)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"create conversation record failed: %v\", err)\n\t\t\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tcontent.WriteString(record.Content)\n\t\tcontent.WriteString(\"\\n\")\n\t}\n\taiRecord := &entity.AIConversationRecord{\n\t\tConversationID:   conversationID,\n\t\tChatCompletionID: chatcmplID,\n\t\tRole:             \"assistant\",\n\t\tContent:          content.String(),\n\t\tHelpful:          0,\n\t\tUnhelpful:        0,\n\t}\n\n\terr = s.aiConversationRepo.CreateRecord(ctx, aiRecord)\n\tif err != nil {\n\t\tlog.Errorf(\"create conversation record failed: %v\", err)\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\tconversation.UpdatedAt = time.Now()\n\terr = s.aiConversationRepo.UpdateConversation(ctx, conversation)\n\tif err != nil {\n\t\tlog.Errorf(\"update conversation failed: %v\", err)\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\treturn nil\n}\n\n// GetConversationList\nfunc (s *aiConversationService) GetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error) {\n\tconversations, total, err := s.aiConversationRepo.GetConversationsPage(ctx, req.Page, req.PageSize, &entity.AIConversation{UserID: req.UserID})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\tlist := make([]schema.AIConversationListItem, 0, len(conversations))\n\tfor _, conversation := range conversations {\n\t\tlist = append(list, schema.AIConversationListItem{\n\t\t\tConversationID: conversation.ConversationID,\n\t\t\tCreatedAt:      conversation.CreatedAt.Unix(),\n\t\t\tTopic:          conversation.Topic,\n\t\t})\n\t}\n\n\treturn pager.NewPageModel(total, list), nil\n}\n\n// GetConversationDetail\nfunc (s *aiConversationService) GetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) (\n\tresp *schema.AIConversationDetailResp, exist bool, err error) {\n\tconversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist || conversation.UserID != req.UserID {\n\t\treturn nil, false, nil\n\t}\n\n\trecords, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, false, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\trecordList := make([]*schema.AIConversationRecord, 0, len(records))\n\tfor i, record := range records {\n\t\tif i == 0 {\n\t\t\trecord.Content = conversation.Topic\n\t\t}\n\t\trecordList = append(recordList, &schema.AIConversationRecord{\n\t\t\tChatCompletionID: record.ChatCompletionID,\n\t\t\tRole:             record.Role,\n\t\t\tContent:          record.Content,\n\t\t\tHelpful:          record.Helpful,\n\t\t\tUnhelpful:        record.Unhelpful,\n\t\t\tCreatedAt:        record.CreatedAt.Unix(),\n\t\t})\n\t}\n\n\treturn &schema.AIConversationDetailResp{\n\t\tConversationID: conversation.ConversationID,\n\t\tTopic:          conversation.Topic,\n\t\tRecords:        recordList,\n\t\tCreatedAt:      conversation.CreatedAt.Unix(),\n\t\tUpdatedAt:      conversation.UpdatedAt.Unix(),\n\t}, true, nil\n}\n\n// VoteRecord\nfunc (s *aiConversationService) VoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error {\n\trecord, exist, err := s.aiConversationRepo.GetRecordByChatCompletionID(ctx, \"assistant\", req.ChatCompletionID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tconversation, exist, err := s.aiConversationRepo.GetConversation(ctx, record.ConversationID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tif conversation.UserID != req.UserID {\n\t\treturn errors.Forbidden(reason.UnauthorizedError)\n\t}\n\n\tif record.Role != \"assistant\" {\n\t\treturn errors.BadRequest(\"Only AI responses can be voted\")\n\t}\n\n\tif req.VoteType == \"helpful\" {\n\t\tif req.Cancel {\n\t\t\trecord.Helpful = 0\n\t\t} else {\n\t\t\trecord.Helpful = 1\n\t\t\trecord.Unhelpful = 0\n\t\t}\n\t} else {\n\t\tif req.Cancel {\n\t\t\trecord.Unhelpful = 0\n\t\t} else {\n\t\t\trecord.Unhelpful = 1\n\t\t\trecord.Helpful = 0\n\t\t}\n\t}\n\n\terr = s.aiConversationRepo.UpdateRecordVote(ctx, record)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\treturn nil\n}\n\n// GetConversationListForAdmin\nfunc (s *aiConversationService) GetConversationListForAdmin(\n\tctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error) {\n\tconversations, total, err := s.aiConversationRepo.GetConversationsForAdmin(ctx, req.Page, req.PageSize, &entity.AIConversation{})\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\tlist := make([]*schema.AIConversationAdminListItem, 0, len(conversations))\n\tfor _, conversation := range conversations {\n\t\tuserInfo, err := s.getUserInfo(ctx, conversation.UserID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get user info failed for user %s: %v\", conversation.UserID, err)\n\t\t\tcontinue\n\t\t}\n\n\t\thelpful, unhelpful, err := s.aiConversationRepo.GetConversationWithVoteStats(ctx, conversation.ConversationID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get conversation vote stats failed for conversation %s: %v\", conversation.ConversationID, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tlist = append(list, &schema.AIConversationAdminListItem{\n\t\t\tID:             conversation.ConversationID,\n\t\t\tTopic:          conversation.Topic,\n\t\t\tUserInfo:       userInfo,\n\t\t\tHelpfulCount:   helpful,\n\t\t\tUnhelpfulCount: unhelpful,\n\t\t\tCreatedAt:      conversation.CreatedAt.Unix(),\n\t\t})\n\t}\n\n\treturn pager.NewPageModel(total, list), nil\n}\n\n// GetConversationDetailForAdmin\nfunc (s *aiConversationService) GetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error) {\n\tconversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tuserInfo, err := s.getUserInfo(ctx, conversation.UserID)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\trecords, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\trecordList := make([]schema.AIConversationRecord, 0, len(records))\n\tfor i, record := range records {\n\t\tif i == 0 {\n\t\t\trecord.Content = conversation.Topic\n\t\t}\n\t\trecordList = append(recordList, schema.AIConversationRecord{\n\t\t\tChatCompletionID: record.ChatCompletionID,\n\t\t\tRole:             record.Role,\n\t\t\tContent:          record.Content,\n\t\t\tHelpful:          record.Helpful,\n\t\t\tUnhelpful:        record.Unhelpful,\n\t\t\tCreatedAt:        record.CreatedAt.Unix(),\n\t\t})\n\t}\n\n\treturn &schema.AIConversationAdminDetailResp{\n\t\tConversationID: conversation.ConversationID,\n\t\tTopic:          conversation.Topic,\n\t\tUserInfo:       userInfo,\n\t\tRecords:        recordList,\n\t\tCreatedAt:      conversation.CreatedAt.Unix(),\n\t}, nil\n}\n\n// getUserInfo\nfunc (s *aiConversationService) getUserInfo(ctx context.Context, userID string) (schema.AIConversationUserInfo, error) {\n\tuserInfo := schema.AIConversationUserInfo{}\n\n\tuser, exist, err := s.userCommon.GetUserBasicInfoByID(ctx, userID)\n\tif err != nil {\n\t\treturn userInfo, err\n\t}\n\tif !exist {\n\t\treturn userInfo, errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tuserInfo.ID = user.ID\n\tuserInfo.Username = user.Username\n\tuserInfo.DisplayName = user.DisplayName\n\tuserInfo.Avatar = user.Avatar\n\tuserInfo.Rank = user.Rank\n\treturn userInfo, nil\n}\n\n// DeleteConversationForAdmin\nfunc (s *aiConversationService) DeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error {\n\t_, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\tif err := s.aiConversationRepo.DeleteConversation(ctx, req.ConversationID); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/answer_common/answer.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage answercommon\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\ntype AnswerRepo interface {\n\tAddAnswer(ctx context.Context, answer *entity.Answer) (err error)\n\tRemoveAnswer(ctx context.Context, id string) (err error)\n\tRecoverAnswer(ctx context.Context, answerID string) (err error)\n\tUpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error)\n\tGetAnswer(ctx context.Context, id string) (answer *entity.Answer, exist bool, err error)\n\tGetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error)\n\tGetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error)\n\tUpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error\n\tGetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error)\n\tGetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error)\n\tGetCountByQuestionID(ctx context.Context, questionID string) (int64, error)\n\tGetCountByUserID(ctx context.Context, userID string) (int64, error)\n\tGetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error)\n\tSearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)\n\tGetPersonalAnswerPage(ctx context.Context, cond *entity.PersonalAnswerPageQueryCond) (\n\t\tresp []*entity.Answer, total int64, err error)\n\tAdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error)\n\tUpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error)\n\tGetAnswerCount(ctx context.Context) (count int64, err error)\n\tRemoveAllUserAnswer(ctx context.Context, userID string) (err error)\n\tSumVotesByQuestionID(ctx context.Context, questionID string) (float64, error)\n\tDeletePermanentlyAnswers(ctx context.Context) (err error)\n}\n\n// AnswerCommon user service\ntype AnswerCommon struct {\n\tanswerRepo AnswerRepo\n}\n\nfunc NewAnswerCommon(answerRepo AnswerRepo) *AnswerCommon {\n\treturn &AnswerCommon{\n\t\tanswerRepo: answerRepo,\n\t}\n}\n\nfunc (as *AnswerCommon) SearchAnswerIDs(ctx context.Context, userID, questionID string) ([]string, error) {\n\tids, err := as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userID, questionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ids, nil\n}\n\nfunc (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) (\n\tresp []*entity.Answer, count int64, err error) {\n\tresp, count, err = as.answerRepo.AdminSearchList(ctx, req)\n\tif handler.GetEnableShortID(ctx) {\n\t\tfor _, item := range resp {\n\t\t\titem.ID = uid.EnShortID(item.ID)\n\t\t\titem.QuestionID = uid.EnShortID(item.QuestionID)\n\t\t}\n\t}\n\treturn resp, count, err\n}\n\nfunc (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {\n\tlist, count, err := as.answerRepo.SearchList(ctx, search)\n\tif err != nil {\n\t\treturn list, count, err\n\t}\n\treturn list, count, err\n}\n\nfunc (as *AnswerCommon) PersonalAnswerPage(ctx context.Context,\n\tcond *entity.PersonalAnswerPageQueryCond) ([]*entity.Answer, int64, error) {\n\treturn as.answerRepo.GetPersonalAnswerPage(ctx, cond)\n}\n\nfunc (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo {\n\tinfo := schema.AnswerInfo{}\n\tinfo.ID = data.ID\n\tinfo.QuestionID = data.QuestionID\n\tinfo.Content = data.OriginalText\n\tinfo.HTML = data.ParsedText\n\tinfo.Accepted = data.Accepted\n\tinfo.VoteCount = data.VoteCount\n\tinfo.CreateTime = data.CreatedAt.Unix()\n\tinfo.UpdateTime = data.UpdatedAt.Unix()\n\tif data.UpdatedAt.Unix() < 1 {\n\t\tinfo.UpdateTime = 0\n\t}\n\tinfo.UserID = data.UserID\n\tinfo.UpdateUserID = data.LastEditUserID\n\tinfo.Status = data.Status\n\tinfo.MemberActions = make([]*schema.PermissionMemberAction, 0)\n\treturn &info\n}\n\nfunc (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer) *schema.AdminAnswerInfo {\n\tinfo := schema.AdminAnswerInfo{}\n\tinfo.ID = data.ID\n\tinfo.QuestionID = data.QuestionID\n\tinfo.Accepted = data.Accepted\n\tinfo.VoteCount = data.VoteCount\n\tinfo.CreateTime = data.CreatedAt.Unix()\n\tinfo.UpdateTime = data.UpdatedAt.Unix()\n\tif data.UpdatedAt.Unix() < 1 {\n\t\tinfo.UpdateTime = 0\n\t}\n\tinfo.UserID = data.UserID\n\tinfo.UpdateUserID = data.LastEditUserID\n\tinfo.Description = htmltext.FetchExcerpt(data.ParsedText, \"...\", 240)\n\treturn &info\n}\n"
  },
  {
    "path": "internal/service/apikey/apikey_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage apikey\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/token\"\n)\n\ntype APIKeyRepo interface {\n\tGetAPIKeyList(ctx context.Context) (keys []*entity.APIKey, err error)\n\tGetAPIKey(ctx context.Context, apiKey string) (key *entity.APIKey, exist bool, err error)\n\tUpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error)\n\tAddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error)\n\tDeleteAPIKey(ctx context.Context, id int) (err error)\n}\n\ntype APIKeyService struct {\n\tapiKeyRepo APIKeyRepo\n}\n\nfunc NewAPIKeyService(\n\tapiKeyRepo APIKeyRepo,\n) *APIKeyService {\n\treturn &APIKeyService{\n\t\tapiKeyRepo: apiKeyRepo,\n\t}\n}\n\nfunc (s *APIKeyService) GetAPIKeyList(ctx context.Context, req *schema.GetAPIKeyReq) (resp []*schema.GetAPIKeyResp, err error) {\n\tkeys, err := s.apiKeyRepo.GetAPIKeyList(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = make([]*schema.GetAPIKeyResp, 0)\n\tfor _, key := range keys {\n\t\t// hide access key middle part, replace with *\n\t\tif len(key.AccessKey) < 10 {\n\t\t\t// If the access key is too short, do not mask it\n\t\t\tkey.AccessKey = strings.Repeat(\"*\", len(key.AccessKey))\n\t\t} else {\n\t\t\tkey.AccessKey = key.AccessKey[:7] + strings.Repeat(\"*\", 8) + key.AccessKey[len(key.AccessKey)-4:]\n\t\t}\n\n\t\tresp = append(resp, &schema.GetAPIKeyResp{\n\t\t\tID:          key.ID,\n\t\t\tAccessKey:   key.AccessKey,\n\t\t\tDescription: key.Description,\n\t\t\tScope:       key.Scope,\n\t\t\tCreatedAt:   key.CreatedAt.Unix(),\n\t\t\tLastUsedAt:  key.LastUsedAt.Unix(),\n\t\t})\n\t}\n\treturn resp, nil\n}\n\nfunc (s *APIKeyService) UpdateAPIKey(ctx context.Context, req *schema.UpdateAPIKeyReq) (err error) {\n\tapiKey := entity.APIKey{\n\t\tID:          req.ID,\n\t\tDescription: req.Description,\n\t}\n\terr = s.apiKeyRepo.UpdateAPIKey(ctx, apiKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *APIKeyService) AddAPIKey(ctx context.Context, req *schema.AddAPIKeyReq) (resp *schema.AddAPIKeyResp, err error) {\n\tak := \"sk_\" + strings.ReplaceAll(token.GenerateToken(), \"-\", \"\")\n\tapiKey := entity.APIKey{\n\t\tDescription: req.Description,\n\t\tAccessKey:   ak,\n\t\tScope:       req.Scope,\n\t\tLastUsedAt:  time.Now(),\n\t\tUserID:      req.UserID,\n\t}\n\terr = s.apiKeyRepo.AddAPIKey(ctx, apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &schema.AddAPIKeyResp{\n\t\tAccessKey: apiKey.AccessKey,\n\t}\n\treturn resp, nil\n}\n\nfunc (s *APIKeyService) DeleteAPIKey(ctx context.Context, req *schema.DeleteAPIKeyReq) (err error) {\n\terr = s.apiKeyRepo.DeleteAPIKey(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/auth/auth.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage auth\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/apikey\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// AuthRepo auth repository\ntype AuthRepo interface {\n\tGetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)\n\tSetUserCacheInfo(ctx context.Context, accessToken, visitToken string, userInfo *entity.UserCacheInfo) error\n\tGetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error)\n\tRemoveUserCacheInfo(ctx context.Context, accessToken string) (err error)\n\tRemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error)\n\tSetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error)\n\tGetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error)\n\tRemoveUserStatus(ctx context.Context, userID string) (err error)\n\tGetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)\n\tSetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error\n\tRemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)\n\tAddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)\n\tRemoveUserTokens(ctx context.Context, userID string, remainToken string)\n}\n\n// AuthService kit service\ntype AuthService struct {\n\tauthRepo   AuthRepo\n\tapiKeyRepo apikey.APIKeyRepo\n}\n\n// NewAuthService email service\nfunc NewAuthService(authRepo AuthRepo, apiKeyRepo apikey.APIKeyRepo) *AuthService {\n\treturn &AuthService{\n\t\tauthRepo:   authRepo,\n\t\tapiKeyRepo: apiKeyRepo,\n\t}\n}\n\nfunc (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {\n\tuserCacheInfo, err := as.authRepo.GetUserCacheInfo(ctx, accessToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userCacheInfo == nil {\n\t\treturn nil, nil\n\t}\n\tcacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)\n\tif cacheInfo != nil {\n\t\tuserCacheInfo.UserStatus = cacheInfo.UserStatus\n\t\tuserCacheInfo.EmailStatus = cacheInfo.EmailStatus\n\t\tuserCacheInfo.RoleID = cacheInfo.RoleID\n\t\t// update current user cache info\n\t\terr := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo.VisitToken, userCacheInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// try to get user status from user center\n\tuc, ok := plugin.GetUserCenter()\n\tif ok && len(userCacheInfo.ExternalID) > 0 {\n\t\tif userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable {\n\t\t\tuserCacheInfo.UserStatus = int(userStatus)\n\t\t}\n\t}\n\treturn userCacheInfo, nil\n}\n\nfunc (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.UserCacheInfo) (\n\taccessToken string, visitToken string, err error) {\n\taccessToken = token.GenerateToken()\n\tvisitToken = token.GenerateToken()\n\terr = as.authRepo.SetUserCacheInfo(ctx, accessToken, visitToken, userInfo)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\treturn accessToken, visitToken, err\n}\n\nfunc (as *AuthService) CheckUserVisitToken(ctx context.Context, visitToken string) bool {\n\taccessToken, err := as.authRepo.GetUserVisitCacheInfo(ctx, visitToken)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif len(accessToken) == 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) {\n\treturn as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo)\n}\n\nfunc (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) {\n\treturn as.authRepo.RemoveUserCacheInfo(ctx, accessToken)\n}\n\nfunc (as *AuthService) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) {\n\tif len(visitToken) > 0 {\n\t\treturn as.authRepo.RemoveUserVisitCacheInfo(ctx, visitToken)\n\t}\n\treturn nil\n}\n\n// AddUserTokenMapping add user token mapping\nfunc (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {\n\treturn as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)\n}\n\n// RemoveUserAllTokens Log out all users under this user id\nfunc (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) {\n\tas.authRepo.RemoveUserTokens(ctx, userID, \"\")\n}\n\n// RemoveTokensExceptCurrentUser remove all tokens except the current user\nfunc (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) {\n\tas.authRepo.RemoveUserTokens(ctx, userID, accessToken)\n}\n\n// Admin\n\nfunc (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {\n\treturn as.authRepo.GetAdminUserCacheInfo(ctx, accessToken)\n}\n\nfunc (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {\n\terr = as.authRepo.SetAdminUserCacheInfo(ctx, accessToken, userInfo)\n\treturn err\n}\n\nfunc (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) {\n\treturn as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken)\n}\nfunc (as *AuthService) AuthAPIKey(ctx context.Context, read bool, apiKey string) (pass bool, err error) {\n\tapiKeyInfo, exist, err := as.apiKeyRepo.GetAPIKey(ctx, apiKey)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !exist {\n\t\treturn false, nil\n\t}\n\t// If the request is not read-only, check if the API key has write permissions\n\tif !read && apiKeyInfo.Scope == \"read-only\" {\n\t\tlog.Warnf(\"API key %s does not have write permissions\", apiKeyInfo.AccessKey)\n\t\treturn false, nil\n\t}\n\tlog.Infof(\"API key %s is valid, scope: %s\", apiKeyInfo.AccessKey, apiKeyInfo.Scope)\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/service/badge/badge_award_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype BadgeAwardRepo interface {\n\tCheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool, err error)\n\tAwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error)\n\n\tCountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64)\n\tCountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error)\n\n\tSumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error)\n\n\tListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error)\n\tListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error)\n\tListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error)\n\n\tGetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error)\n\tGetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error)\n\n\tDeleteUserBadgeAward(ctx context.Context, userID string) (err error)\n}\n\ntype BadgeAwardService struct {\n\tbadgeAwardRepo           BadgeAwardRepo\n\tbadgeRepo                BadgeRepo\n\tuserCommon               *usercommon.UserCommon\n\tobjectInfoService        *object_info.ObjService\n\tnotificationQueueService noticequeue.Service\n}\n\nfunc NewBadgeAwardService(\n\tbadgeAwardRepo BadgeAwardRepo,\n\tbadgeRepo BadgeRepo,\n\tuserCommon *usercommon.UserCommon,\n\tobjectInfoService *object_info.ObjService,\n\tnotificationQueueService noticequeue.Service,\n) *BadgeAwardService {\n\treturn &BadgeAwardService{\n\t\tbadgeAwardRepo:           badgeAwardRepo,\n\t\tbadgeRepo:                badgeRepo,\n\t\tuserCommon:               userCommon,\n\t\tobjectInfoService:        objectInfoService,\n\t\tnotificationQueueService: notificationQueueService,\n\t}\n}\n\n// GetBadgeAwardList get badge award list\nfunc (bs *BadgeAwardService) GetBadgeAwardList(\n\tctx context.Context,\n\treq *schema.GetBadgeAwardWithPageReq,\n) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) {\n\tvar (\n\t\tbadgeAwardList []*entity.BadgeAward\n\t)\n\n\treq.UserID, err = bs.validateUserByUsername(ctx, req.Username)\n\tif err != nil {\n\t\tbadgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize)\n\t} else {\n\t\tbadgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize)\n\t}\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresp = make([]*schema.GetBadgeAwardWithPageResp, len(badgeAwardList))\n\n\tfor i, badgeAward := range badgeAwardList {\n\t\tvar (\n\t\t\tobjectID, questionID, answerID, commentID, objectType, urlTitle string\n\t\t)\n\n\t\t// if exist object info\n\t\tobjInfo, e := bs.objectInfoService.GetInfo(ctx, badgeAward.AwardKey)\n\t\tif e == nil && !objInfo.IsDeleted() {\n\t\t\tobjectID = objInfo.ObjectID\n\t\t\tquestionID = objInfo.QuestionID\n\t\t\tanswerID = objInfo.AnswerID\n\t\t\tcommentID = objInfo.CommentID\n\t\t\tobjectType = objInfo.ObjectType\n\t\t\turlTitle = objInfo.Title\n\t\t}\n\n\t\trow := &schema.GetBadgeAwardWithPageResp{\n\t\t\tCreatedAt:      badgeAward.CreatedAt.Unix(),\n\t\t\tObjectID:       objectID,\n\t\t\tQuestionID:     questionID,\n\t\t\tAnswerID:       answerID,\n\t\t\tCommentID:      commentID,\n\t\t\tObjectType:     objectType,\n\t\t\tUrlTitle:       urlTitle,\n\t\t\tAuthorUserInfo: schema.UserBasicInfo{},\n\t\t}\n\n\t\t// get user info\n\t\tuserInfo, exists, e := bs.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID)\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"user not found by id: %s, err: %v\", badgeAward.UserID, e)\n\t\t}\n\t\tif exists {\n\t\t\t_ = copier.Copy(&row.AuthorUserInfo, userInfo)\n\t\t}\n\n\t\tresp[i] = row\n\t}\n\n\treturn\n}\n\n// Award award badge\nfunc (bs *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) {\n\tbadgeData, exists, err := bs.badgeRepo.GetByID(ctx, badgeID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !exists || badgeData.Status == entity.BadgeStatusInactive {\n\t\treturn errors.BadRequest(reason.BadgeObjectNotFound)\n\t}\n\n\talreadyAwarded, err := bs.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif alreadyAwarded {\n\t\treturn nil\n\t}\n\n\tbadgeAward := &entity.BadgeAward{\n\t\tUserID:         userID,\n\t\tBadgeID:        badgeID,\n\t\tAwardKey:       awardKey,\n\t\tBadgeGroupID:   badgeData.BadgeGroupID,\n\t\tIsBadgeDeleted: entity.IsBadgeNotDeleted,\n\t}\n\terr = bs.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmsg := &schema.NotificationMsg{\n\t\tTriggerUserID:      badgeAward.UserID,\n\t\tReceiverUserID:     badgeAward.UserID,\n\t\tType:               schema.NotificationTypeAchievement,\n\t\tObjectID:           badgeAward.ID,\n\t\tObjectType:         constant.BadgeAwardObjectType,\n\t\tTitle:              badgeData.Name,\n\t\tExtraInfo:          map[string]string{\"badge_id\": badgeData.ID},\n\t\tNotificationAction: constant.NotificationEarnedBadge,\n\t}\n\tbs.notificationQueueService.Send(ctx, msg)\n\treturn nil\n}\n\n// GetUserBadgeAwardList get user badge award list\nfunc (bs *BadgeAwardService) GetUserBadgeAwardList(\n\tctx *gin.Context,\n\treq *schema.GetUserBadgeAwardListReq,\n) (\n\tresp []*schema.GetUserBadgeAwardListResp,\n\ttotal int64,\n\terr error,\n) {\n\tvar (\n\t\tearnedCounts []*entity.BadgeEarnedCount\n\t)\n\n\treq.UserID, err = bs.validateUserByUsername(ctx, req.Username)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tearnedCounts, err = bs.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn\n\t}\n\ttotal = int64(len(earnedCounts))\n\tresp = make([]*schema.GetUserBadgeAwardListResp, total)\n\n\tfor i, earnedCount := range earnedCounts {\n\t\tbadge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID)\n\t\tif e != nil {\n\t\t\terr = e\n\t\t\treturn\n\t\t}\n\t\tif !exists {\n\t\t\tcontinue\n\t\t}\n\t\tresp[i] = &schema.GetUserBadgeAwardListResp{\n\t\t\tID:          uid.EnShortID(badge.ID),\n\t\t\tName:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),\n\t\t\tIcon:        badge.Icon,\n\t\t\tEarnedCount: earnedCount.EarnedCount,\n\t\t\tLevel:       badge.Level,\n\t\t}\n\t}\n\n\treturn\n}\n\n// GetUserRecentBadgeAwardList get user badge award list\nfunc (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req *schema.GetUserBadgeAwardListReq) (\n\tresp []*schema.GetUserBadgeAwardListResp, total int64, err error) {\n\tvar (\n\t\tearnedCounts []*entity.BadgeAwardRecent\n\t)\n\n\treq.UserID, err = bs.validateUserByUsername(ctx, req.Username)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tearnedCounts, err = bs.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttotal = int64(len(earnedCounts))\n\tresp = make([]*schema.GetUserBadgeAwardListResp, total)\n\n\tfor i, earnedCount := range earnedCounts {\n\t\tbadge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID)\n\t\tif e != nil {\n\t\t\terr = e\n\t\t\treturn\n\t\t}\n\t\tif !exists {\n\t\t\tcontinue\n\t\t}\n\t\tresp[i] = &schema.GetUserBadgeAwardListResp{\n\t\t\tID:          uid.EnShortID(badge.ID),\n\t\t\tName:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),\n\t\t\tIcon:        badge.Icon,\n\t\t\tEarnedCount: earnedCount.EarnedCount,\n\t\t\tLevel:       badge.Level,\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) {\n\tvar (\n\t\tuserInfo *schema.UserBasicInfo\n\t\texist    bool\n\t)\n\t// validate user exists or not\n\tif len(userName) > 0 {\n\t\tuserInfo, exist, err = bs.userCommon.GetUserBasicInfoByUserName(ctx, userName)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tif !exist {\n\t\t\terr = errors.BadRequest(reason.UserNotFound)\n\t\t\treturn\n\t\t}\n\t\tuserID = userInfo.ID\n\t}\n\tif len(userID) == 0 {\n\t\terr = errors.BadRequest(reason.UserNotFound)\n\t\treturn\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/service/badge/badge_event_handler.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype BadgeEventService struct {\n\tdata              *data.Data\n\teventQueueService eventqueue.Service\n\tbadgeRepo         BadgeRepo\n\teventRuleRepo     EventRuleRepo\n\tbadgeAwardService *BadgeAwardService\n}\n\ntype EventRuleHandler func(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error)\n\ntype EventRuleRepo interface {\n\tHandleEventWithRule(ctx context.Context, msg *schema.EventMsg) (awards []*entity.BadgeAward)\n}\n\nfunc NewBadgeEventService(\n\tdata *data.Data,\n\teventQueueService eventqueue.Service,\n\tbadgeRepo BadgeRepo,\n\teventRuleRepo EventRuleRepo,\n\tbadgeAwardService *BadgeAwardService,\n) *BadgeEventService {\n\tn := &BadgeEventService{\n\t\tdata:              data,\n\t\teventQueueService: eventQueueService,\n\t\tbadgeRepo:         badgeRepo,\n\t\teventRuleRepo:     eventRuleRepo,\n\t\tbadgeAwardService: badgeAwardService,\n\t}\n\teventQueueService.RegisterHandler(n.Handler)\n\treturn n\n}\n\nfunc (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error {\n\tawards := ns.eventRuleRepo.HandleEventWithRule(ctx, msg)\n\tif len(awards) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, award := range awards {\n\t\terr := ns.badgeAwardService.Award(ctx, award.BadgeID, award.UserID, award.AwardKey)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"error awarding badge %s: %v\", award.BadgeID, err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/badge/badge_group_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\ntype BadgeGroupRepo interface {\n\tListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error)\n\tAddGroup(ctx context.Context, group *entity.BadgeGroup) (err error)\n}\n\ntype BadgeGroupService struct {\n\tbadgeGroupRepo BadgeGroupRepo\n}\n\nfunc NewBadgeGroupService(badgeGroupRepo BadgeGroupRepo) *BadgeGroupService {\n\treturn &BadgeGroupService{\n\t\tbadgeGroupRepo: badgeGroupRepo,\n\t}\n}\n"
  },
  {
    "path": "internal/service/badge/badge_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage badge\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype BadgeRepo interface {\n\tGetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error)\n\tGetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error)\n\n\tListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)\n\tListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)\n\tListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)\n\n\tUpdateStatus(ctx context.Context, id string, status int8) (err error)\n\tUpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error)\n}\n\ntype BadgeService struct {\n\tbadgeRepo             BadgeRepo\n\tbadgeGroupRepo        BadgeGroupRepo\n\tbadgeAwardRepo        BadgeAwardRepo\n\tbadgeEventService     *BadgeEventService\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n}\n\nfunc NewBadgeService(\n\tbadgeRepo BadgeRepo,\n\tbadgeGroupRepo BadgeGroupRepo,\n\tbadgeAwardRepo BadgeAwardRepo,\n\tbadgeEventService *BadgeEventService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n) *BadgeService {\n\treturn &BadgeService{\n\t\tbadgeRepo:             badgeRepo,\n\t\tbadgeGroupRepo:        badgeGroupRepo,\n\t\tbadgeAwardRepo:        badgeAwardRepo,\n\t\tbadgeEventService:     badgeEventService,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t}\n}\n\n// ListByGroup list all badges group by group\nfunc (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) {\n\tvar (\n\t\tgroups       []*entity.BadgeGroup\n\t\tbadges       []*entity.Badge\n\t\tearnedCounts []*entity.BadgeEarnedCount\n\n\t\tgroupMap  = make(map[int64]string, 0)\n\t\tbadgesMap = make(map[int64][]*schema.BadgeListInfo, 0)\n\t)\n\tresp = make([]*schema.GetBadgeListResp, 0)\n\n\tgroups, err = b.badgeGroupRepo.ListGroups(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tbadges, _, err = b.badgeRepo.ListActivated(ctx, 0, 0)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif len(userID) > 0 {\n\t\tearnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor _, group := range groups {\n\t\tgroupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name)\n\t}\n\n\tfor _, badge := range badges {\n\t\t// check is earned\n\t\tvar earned int64 = 0\n\t\tif len(earnedCounts) > 0 {\n\t\t\tfor _, earnedCount := range earnedCounts {\n\t\t\t\tif badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 {\n\t\t\t\t\tearned = earnedCount.EarnedCount\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tbadgesMap[badge.BadgeGroupID] = append(badgesMap[badge.BadgeGroupID], &schema.BadgeListInfo{\n\t\t\tID:          uid.EnShortID(badge.ID),\n\t\t\tName:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),\n\t\t\tIcon:        badge.Icon,\n\t\t\tAwardCount:  badge.AwardCount,\n\t\t\tEarnedCount: earned,\n\t\t\tLevel:       badge.Level,\n\t\t})\n\t}\n\n\tfor _, group := range groups {\n\t\tresp = append(resp, &schema.GetBadgeListResp{\n\t\t\tGroupName: translator.Tr(handler.GetLangByCtx(ctx), group.Name),\n\t\t\tBadges:    badgesMap[converter.StringToInt64(group.ID)],\n\t\t})\n\t}\n\n\treturn\n}\n\n// ListPaged list all badges by page\nfunc (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPagedReq) (resp []*schema.GetBadgeListPagedResp, total int64, err error) {\n\tvar (\n\t\tgroups   []*entity.BadgeGroup\n\t\tbadges   []*entity.Badge\n\t\tbadge    *entity.Badge\n\t\texists   bool\n\t\tgroupMap = make(map[int64]string, 0)\n\t)\n\n\ttotal = 0\n\n\tif len(req.Query) > 0 {\n\t\tisID := strings.Index(req.Query, \"badge:\")\n\t\tif isID != 0 {\n\t\t\tbadges, err = b.searchByName(ctx, req.Query)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// paged result\n\t\t\tcount := len(badges)\n\t\t\ttotal = int64(count)\n\t\t\tstart := (req.Page - 1) * req.PageSize\n\t\t\tend := req.Page * req.PageSize\n\t\t\tif start >= count {\n\t\t\t\tstart = count\n\t\t\t\tend = count\n\t\t\t}\n\t\t\tif end > count {\n\t\t\t\tend = count\n\t\t\t}\n\t\t\tbadges = badges[start:end]\n\t\t} else {\n\t\t\treq.Query = strings.TrimSpace(strings.TrimLeft(req.Query, \"badge:\"))\n\t\t\tid := uid.DeShortID(req.Query)\n\t\t\tif len(id) == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbadge, exists, err = b.badgeRepo.GetByID(ctx, id)\n\t\t\tif err != nil || !exists {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbadges = append(badges, badge)\n\t\t}\n\t} else {\n\t\tswitch req.Status {\n\t\tcase schema.BadgeStatusActive:\n\t\t\tbadges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize)\n\t\tcase schema.BadgeStatusInactive:\n\t\t\tbadges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize)\n\t\tdefault:\n\t\t\tbadges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// find all group and build group map\n\tgroups, err = b.badgeGroupRepo.ListGroups(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, group := range groups {\n\t\tgroupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name)\n\t}\n\n\tresp = make([]*schema.GetBadgeListPagedResp, len(badges))\n\n\tgeneral, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx)\n\tbaseURL := \"\"\n\tif siteErr == nil {\n\t\tbaseURL = general.SiteUrl\n\t}\n\n\tfor i, badge := range badges {\n\t\tresp[i] = &schema.GetBadgeListPagedResp{\n\t\t\tID:          uid.EnShortID(badge.ID),\n\t\t\tName:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),\n\t\t\tDescription: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + \"/users/settings/profile\"}),\n\t\t\tIcon:        badge.Icon,\n\t\t\tAwardCount:  badge.AwardCount,\n\t\t\tLevel:       badge.Level,\n\t\t\tGroupName:   groupMap[badge.BadgeGroupID],\n\t\t\tStatus:      schema.BadgeStatusMap[badge.Status],\n\t\t}\n\t}\n\treturn\n}\n\n// searchByName\nfunc (b *BadgeService) searchByName(ctx context.Context, name string) (result []*entity.Badge, err error) {\n\tvar badges []*entity.Badge\n\tname = strings.ToLower(name)\n\tresult = make([]*entity.Badge, 0)\n\n\tbadges, _, err = b.badgeRepo.ListPaged(ctx, 0, 0)\n\tfor _, badge := range badges {\n\t\ttn := strings.ToLower(translator.Tr(handler.GetLangByCtx(ctx), badge.Name))\n\t\tif strings.Contains(tn, name) {\n\t\t\tresult = append(result, badge)\n\t\t}\n\t}\n\treturn\n}\n\n// GetBadgeInfo get badge info\nfunc (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) {\n\tbadge, exists, err := b.badgeRepo.GetByID(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !exists || badge.Status == entity.BadgeStatusInactive {\n\t\treturn nil, errors.BadRequest(reason.BadgeObjectNotFound)\n\t}\n\n\tvar earnedTotal int64\n\tif len(userID) > 0 {\n\t\tearnedTotal = b.badgeAwardRepo.CountByUserIdAndBadgeId(ctx, userID, badge.ID)\n\t}\n\n\tbaseURL := \"\"\n\tgeneral, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx)\n\tif siteErr == nil {\n\t\tbaseURL = general.SiteUrl\n\t}\n\n\tinfo = &schema.GetBadgeInfoResp{\n\t\tID:          uid.EnShortID(badge.ID),\n\t\tName:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),\n\t\tDescription: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + \"/users/settings/profile\"}),\n\t\tIcon:        badge.Icon,\n\t\tAwardCount:  badge.AwardCount,\n\t\tEarnedCount: earnedTotal,\n\t\tIsSingle:    badge.Single == entity.BadgeSingleAward,\n\t\tLevel:       badge.Level,\n\t}\n\treturn\n}\n\n// UpdateStatus update badge status\nfunc (b *BadgeService) UpdateStatus(ctx *gin.Context, req *schema.UpdateBadgeStatusReq) (err error) {\n\treq.ID = uid.DeShortID(req.ID)\n\n\tbadge, exists, err := b.badgeRepo.GetByID(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exists {\n\t\treturn errors.BadRequest(reason.BadgeObjectNotFound)\n\t}\n\n\t// check duplicate action\n\tstatus, ok := schema.BadgeStatusEMap[req.Status]\n\tif !ok {\n\t\terr = errors.BadRequest(reason.StatusInvalid)\n\t\treturn\n\t}\n\tif badge.Status == status {\n\t\treturn\n\t}\n\n\terr = b.badgeRepo.UpdateStatus(ctx, req.ID, status)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif status == entity.BadgeStatusActive {\n\t\tcount, err := b.badgeAwardRepo.CountByBadgeID(ctx, badge.ID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"count badge award failed: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t\terr = b.badgeRepo.UpdateAwardCount(ctx, badge.ID, int(count))\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"update badge award count failed: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/collection/collection_group_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage collection\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// CollectionGroupRepo collectionGroup repository\ntype CollectionGroupRepo interface {\n\tAddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error)\n\tAddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error)\n\tCreateDefaultGroupIfNotExist(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error)\n\tUpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error)\n\tGetCollectionGroup(ctx context.Context, id string) (collectionGroup *entity.CollectionGroup, exist bool, err error)\n\tGetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error)\n\tGetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error)\n}\n\n// CollectionGroupService user service\ntype CollectionGroupService struct {\n\tcollectionGroupRepo CollectionGroupRepo\n}\n\nfunc NewCollectionGroupService(collectionGroupRepo CollectionGroupRepo) *CollectionGroupService {\n\treturn &CollectionGroupService{\n\t\tcollectionGroupRepo: collectionGroupRepo,\n\t}\n}\n\n// AddCollectionGroup add collection group\nfunc (cs *CollectionGroupService) AddCollectionGroup(ctx context.Context, req *schema.AddCollectionGroupReq) (err error) {\n\tcollectionGroup := &entity.CollectionGroup{}\n\t_ = copier.Copy(collectionGroup, req)\n\treturn cs.collectionGroupRepo.AddCollectionGroup(ctx, collectionGroup)\n}\n\n// UpdateCollectionGroup update collection group\nfunc (cs *CollectionGroupService) UpdateCollectionGroup(ctx context.Context, req *schema.UpdateCollectionGroupReq, cols []string) (err error) {\n\tcollectionGroup := &entity.CollectionGroup{}\n\t_ = copier.Copy(collectionGroup, req)\n\treturn cs.collectionGroupRepo.UpdateCollectionGroup(ctx, collectionGroup, cols)\n}\n\n// GetCollectionGroup get collection group one\nfunc (cs *CollectionGroupService) GetCollectionGroup(ctx context.Context, id string) (resp *schema.GetCollectionGroupResp, err error) {\n\tcollectionGroup, exist, err := cs.collectionGroupRepo.GetCollectionGroup(ctx, id)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UnknownError)\n\t}\n\n\tresp = &schema.GetCollectionGroupResp{}\n\t_ = copier.Copy(resp, collectionGroup)\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/collection/collection_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage collection\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n)\n\n// CollectionService user service\ntype CollectionService struct {\n\tcollectionRepo      collectioncommon.CollectionRepo\n\tcollectionGroupRepo CollectionGroupRepo\n\tquestionCommon      *questioncommon.QuestionCommon\n}\n\nfunc NewCollectionService(\n\tcollectionRepo collectioncommon.CollectionRepo,\n\tcollectionGroupRepo CollectionGroupRepo,\n\tquestionCommon *questioncommon.QuestionCommon,\n) *CollectionService {\n\treturn &CollectionService{\n\t\tcollectionRepo:      collectionRepo,\n\t\tcollectionGroupRepo: collectionGroupRepo,\n\t\tquestionCommon:      questionCommon,\n\t}\n}\n\nfunc (cs *CollectionService) CollectionSwitch(ctx context.Context, req *schema.CollectionSwitchReq) (\n\tresp *schema.CollectionSwitchResp, err error) {\n\tcollectionGroup, err := cs.collectionGroupRepo.CreateDefaultGroupIfNotExist(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcollection, exist, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, req.UserID, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif (!req.Bookmark && !exist) || (req.Bookmark && exist) {\n\t\treturn nil, nil\n\t}\n\n\tif req.Bookmark {\n\t\tcollection = &entity.Collection{\n\t\t\tUserID:                req.UserID,\n\t\t\tObjectID:              req.ObjectID,\n\t\t\tUserCollectionGroupID: collectionGroup.ID,\n\t\t}\n\t\terr = cs.collectionRepo.AddCollection(ctx, collection)\n\t} else {\n\t\terr = cs.collectionRepo.RemoveCollection(ctx, collection.ID)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// For now, we only support bookmark for question, so we just update question collection count\n\tresp = &schema.CollectionSwitchResp{}\n\tresp.ObjectCollectionCount, err = cs.questionCommon.UpdateCollectionCount(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/collection_common/collection.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage collectioncommon\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// CollectionRepo collection repository\ntype CollectionRepo interface {\n\tAddCollection(ctx context.Context, collection *entity.Collection) (err error)\n\tRemoveCollection(ctx context.Context, id string) (err error)\n\tUpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error)\n\tGetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error)\n\tGetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error)\n\tGetOneByObjectIDAndUser(ctx context.Context, userId string, objectId string) (collection *entity.Collection, exist bool, err error)\n\tSearchByObjectIDsAndUser(ctx context.Context, userId string, objectIds []string) (collectionList []*entity.Collection, err error)\n\tCountByObjectID(ctx context.Context, objectId string) (total int64, err error)\n\tGetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error)\n\tSearchObjectCollected(ctx context.Context, userId string, objectIds []string) (collectedMap map[string]bool, err error)\n\tSearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error)\n}\n\n// CollectionCommon user service\ntype CollectionCommon struct {\n\tcollectionRepo CollectionRepo\n}\n\nfunc NewCollectionCommon(collectionRepo CollectionRepo) *CollectionCommon {\n\treturn &CollectionCommon{\n\t\tcollectionRepo: collectionRepo,\n\t}\n}\n\n// SearchObjectCollected search object is collected\nfunc (ccs *CollectionCommon) SearchObjectCollected(ctx context.Context, userId string, objectIds []string) (collectedMap map[string]bool, err error) {\n\treturn ccs.collectionRepo.SearchObjectCollected(ctx, userId, objectIds)\n}\n\nfunc (ccs *CollectionCommon) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) {\n\treturn ccs.collectionRepo.SearchList(ctx, search)\n}\n"
  },
  {
    "path": "internal/service/comment/comment_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage comment\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/apache/answer/internal/service/review\"\n\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// CommentRepo comment repository\ntype CommentRepo interface {\n\tAddComment(ctx context.Context, comment *entity.Comment) (err error)\n\tRemoveComment(ctx context.Context, commentID string) (err error)\n\tUpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error)\n\tUpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)\n\tGetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)\n\tGetCommentPage(ctx context.Context, commentQuery *CommentQuery) (\n\t\tcomments []*entity.Comment, total int64, err error)\n}\n\ntype CommentQuery struct {\n\tpager.PageCond\n\t// object id\n\tObjectID string\n\t// query condition\n\tQueryCond string\n\t// user id\n\tUserID string\n}\n\nfunc (c *CommentQuery) GetOrderBy() string {\n\tif c.QueryCond == \"vote\" {\n\t\treturn \"vote_count DESC,created_at ASC\"\n\t}\n\tif c.QueryCond == \"created_at\" {\n\t\treturn \"created_at DESC\"\n\t}\n\treturn \"created_at ASC\"\n}\n\n// CommentService user service\ntype CommentService struct {\n\tcommentRepo                      CommentRepo\n\tcommentCommonRepo                comment_common.CommentCommonRepo\n\tuserCommon                       *usercommon.UserCommon\n\tvoteCommon                       activity_common.VoteRepo\n\tobjectInfoService                *object_info.ObjService\n\temailService                     *export.EmailService\n\tuserRepo                         usercommon.UserRepo\n\tnotificationQueueService         noticequeue.Service\n\texternalNotificationQueueService noticequeue.ExternalService\n\tactivityQueueService             activityqueue.Service\n\teventQueueService                eventqueue.Service\n\treviewService                    *review.ReviewService\n}\n\n// NewCommentService new comment service\nfunc NewCommentService(\n\tcommentRepo CommentRepo,\n\tcommentCommonRepo comment_common.CommentCommonRepo,\n\tuserCommon *usercommon.UserCommon,\n\tobjectInfoService *object_info.ObjService,\n\tvoteCommon activity_common.VoteRepo,\n\temailService *export.EmailService,\n\tuserRepo usercommon.UserRepo,\n\tnotificationQueueService noticequeue.Service,\n\texternalNotificationQueueService noticequeue.ExternalService,\n\tactivityQueueService activityqueue.Service,\n\teventQueueService eventqueue.Service,\n\treviewService *review.ReviewService,\n) *CommentService {\n\treturn &CommentService{\n\t\tcommentRepo:                      commentRepo,\n\t\tcommentCommonRepo:                commentCommonRepo,\n\t\tuserCommon:                       userCommon,\n\t\tvoteCommon:                       voteCommon,\n\t\tobjectInfoService:                objectInfoService,\n\t\temailService:                     emailService,\n\t\tuserRepo:                         userRepo,\n\t\tnotificationQueueService:         notificationQueueService,\n\t\texternalNotificationQueueService: externalNotificationQueueService,\n\t\tactivityQueueService:             activityQueueService,\n\t\teventQueueService:                eventQueueService,\n\t\treviewService:                    reviewService,\n\t}\n}\n\n// AddComment add comment\nfunc (cs *CommentService) AddComment(ctx context.Context, req *schema.AddCommentReq) (\n\tresp *schema.GetCommentResp, err error) {\n\tcomment := &entity.Comment{}\n\t_ = copier.Copy(comment, req)\n\tcomment.Status = entity.CommentStatusAvailable\n\n\tobjInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif objInfo.IsDeleted() {\n\t\treturn nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)\n\t}\n\tobjInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)\n\tobjInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)\n\tobjInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)\n\tif objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType {\n\t\tcomment.QuestionID = objInfo.QuestionID\n\t}\n\n\tif len(req.ReplyCommentID) > 0 {\n\t\treplyComment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ReplyCommentID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errors.BadRequest(reason.CommentNotFound)\n\t\t}\n\t\tcomment.SetReplyUserID(replyComment.UserID)\n\t\tcomment.SetReplyCommentID(replyComment.ID)\n\t} else {\n\t\tcomment.SetReplyUserID(\"\")\n\t\tcomment.SetReplyCommentID(\"\")\n\t}\n\n\terr = cs.commentRepo.AddComment(ctx, comment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcomment.Status = cs.reviewService.AddCommentReview(ctx, comment, req.IP, req.UserAgent)\n\tif err := cs.commentRepo.UpdateCommentStatus(ctx, comment.ID, comment.Status); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &schema.GetCommentResp{}\n\tresp.SetFromComment(comment)\n\tresp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,\n\t\ttime.Now(), req.CanEdit, req.CanDelete)\n\n\tif comment.Status == entity.CommentStatusAvailable {\n\t\tif err := cs.addCommentNotification(ctx, req, resp, comment, objInfo); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// get user info\n\tuserInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\tresp.Username = userInfo.Username\n\t\tresp.UserDisplayName = userInfo.DisplayName\n\t\tresp.UserAvatar = userInfo.Avatar\n\t\tresp.UserStatus = userInfo.Status\n\t}\n\n\tactivityMsg := &schema.ActivityMsg{\n\t\tUserID:           comment.UserID,\n\t\tObjectID:         comment.ID,\n\t\tOriginalObjectID: req.ObjectID,\n\t\tActivityTypeKey:  constant.ActQuestionCommented,\n\t}\n\tvar event *schema.EventMsg\n\tswitch objInfo.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tactivityMsg.ActivityTypeKey = constant.ActQuestionCommented\n\t\tevent = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).\n\t\t\tCID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID)\n\tcase constant.AnswerObjectType:\n\t\tactivityMsg.ActivityTypeKey = constant.ActAnswerCommented\n\t\tevent = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).\n\t\t\tCID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID)\n\t}\n\tcs.activityQueueService.Send(ctx, activityMsg)\n\tcs.eventQueueService.Send(ctx, event)\n\treturn resp, nil\n}\n\nfunc (cs *CommentService) addCommentNotification(\n\tctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,\n\tcomment *entity.Comment, objInfo *schema.SimpleObjectInfo) error {\n\t// The priority of the notification\n\t// 1. reply to user\n\t// 2. comment mention to user\n\t// 3. answer or question was commented\n\talreadyNotifiedUserID := make(map[string]bool)\n\n\t// get reply user info\n\tif len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {\n\t\treplyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exist {\n\t\t\tresp.ReplyUsername = replyUser.Username\n\t\t\tresp.ReplyUserDisplayName = replyUser.DisplayName\n\t\t\tresp.ReplyUserStatus = replyUser.Status\n\t\t}\n\t\tcs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID,\n\t\t\tobjInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t\talreadyNotifiedUserID[replyUser.ID] = true\n\t\treturn nil\n\t}\n\n\tif len(req.MentionUsernameList) > 0 {\n\t\talreadyNotifiedUserIDs := cs.notificationMention(\n\t\t\tctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID)\n\t\tfor _, userID := range alreadyNotifiedUserIDs {\n\t\t\talreadyNotifiedUserID[userID] = true\n\t\t}\n\t\treturn nil\n\t}\n\n\tif objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {\n\t\tcs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,\n\t\t\tobjInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {\n\t\tcs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,\n\t\t\tobjInfo.ObjectCreatorUserID, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t}\n\treturn nil\n}\n\n// RemoveComment delete comment\nfunc (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {\n\terr = cs.commentRepo.RemoveComment(ctx, req.CommentID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID).\n\t\tTID(req.CommentID).CID(req.CommentID, req.UserID))\n\treturn nil\n}\n\n// UpdateComment update comment\nfunc (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (\n\tresp *schema.UpdateCommentResp, err error) {\n\told, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.CommentNotFound)\n\t}\n\t// user can't edit the comment that was posted by others except admin\n\tif !req.IsAdmin && req.UserID != old.UserID {\n\t\treturn nil, errors.BadRequest(reason.CommentNotFound)\n\t}\n\n\t// user can edit the comment that was posted by himself before deadline.\n\t// admin can edit it at any time\n\tif !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {\n\t\treturn nil, errors.BadRequest(reason.CommentCannotEditAfterDeadline)\n\t}\n\n\tif err = cs.commentRepo.UpdateCommentContent(ctx, old.ID, req.OriginalText, req.ParsedText); err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &schema.UpdateCommentResp{\n\t\tCommentID:    old.ID,\n\t\tOriginalText: req.OriginalText,\n\t\tParsedText:   req.ParsedText,\n\t}\n\tcs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID).\n\t\tCID(old.ID, old.UserID))\n\treturn resp, nil\n}\n\n// GetComment get comment one\nfunc (cs *CommentService) GetComment(ctx context.Context, req *schema.GetCommentReq) (resp *schema.GetCommentResp, err error) {\n\tcomment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.CommentNotFound)\n\t}\n\n\tresp = &schema.GetCommentResp{\n\t\tCommentID:      comment.ID,\n\t\tCreatedAt:      comment.CreatedAt.Unix(),\n\t\tUserID:         comment.UserID,\n\t\tReplyUserID:    comment.GetReplyUserID(),\n\t\tReplyCommentID: comment.GetReplyCommentID(),\n\t\tObjectID:       comment.ObjectID,\n\t\tVoteCount:      comment.VoteCount,\n\t\tOriginalText:   comment.OriginalText,\n\t\tParsedText:     comment.ParsedText,\n\t}\n\n\t// get comment user info\n\tif len(resp.UserID) > 0 {\n\t\tcommentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tresp.Username = commentUser.Username\n\t\t\tresp.UserDisplayName = commentUser.DisplayName\n\t\t\tresp.UserAvatar = commentUser.Avatar\n\t\t\tresp.UserStatus = commentUser.Status\n\t\t}\n\t}\n\n\t// get reply user info\n\tif len(resp.ReplyUserID) > 0 {\n\t\treplyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tresp.ReplyUsername = replyUser.Username\n\t\t\tresp.ReplyUserDisplayName = replyUser.DisplayName\n\t\t\tresp.ReplyUserStatus = replyUser.Status\n\t\t}\n\t}\n\n\t// check if current user vote this comment\n\tresp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)\n\n\tresp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,\n\t\tcomment.CreatedAt, req.CanEdit, req.CanDelete)\n\treturn resp, nil\n}\n\n// GetCommentWithPage get comment list page\nfunc (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tdto := &CommentQuery{\n\t\tPageCond:  pager.PageCond{Page: req.Page, PageSize: req.PageSize},\n\t\tObjectID:  req.ObjectID,\n\t\tQueryCond: req.QueryCond,\n\t}\n\tcommentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := make([]*schema.GetCommentResp, 0)\n\tfor _, comment := range commentList {\n\t\tcommentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp = append(resp, commentResp)\n\t}\n\n\t// if user request the specific comment, add it if not exist.\n\tif len(req.CommentID) > 0 {\n\t\tcommentExist := false\n\t\tfor _, t := range resp {\n\t\t\tif t.CommentID == req.CommentID {\n\t\t\t\tcommentExist = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !commentExist {\n\t\t\tcomment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist && comment.ObjectID == req.ObjectID {\n\t\t\t\tcommentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tresp = append(resp, commentResp)\n\t\t\t}\n\t\t}\n\t}\n\treturn pager.NewPageModel(total, resp), nil\n}\n\nfunc (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq,\n\tcomment *entity.Comment) (commentResp *schema.GetCommentResp, err error) {\n\tcommentResp = &schema.GetCommentResp{\n\t\tCommentID:      comment.ID,\n\t\tCreatedAt:      comment.CreatedAt.Unix(),\n\t\tUserID:         comment.UserID,\n\t\tReplyUserID:    comment.GetReplyUserID(),\n\t\tReplyCommentID: comment.GetReplyCommentID(),\n\t\tObjectID:       comment.ObjectID,\n\t\tVoteCount:      comment.VoteCount,\n\t\tOriginalText:   comment.OriginalText,\n\t\tParsedText:     comment.ParsedText,\n\t}\n\n\t// get comment user info\n\tif len(commentResp.UserID) > 0 {\n\t\tcommentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tcommentResp.Username = commentUser.Username\n\t\t\tcommentResp.UserDisplayName = commentUser.DisplayName\n\t\t\tcommentResp.UserAvatar = commentUser.Avatar\n\t\t\tcommentResp.UserStatus = commentUser.Status\n\t\t}\n\t}\n\n\t// get reply user info\n\tif len(commentResp.ReplyUserID) > 0 {\n\t\treplyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\tcommentResp.ReplyUsername = replyUser.Username\n\t\t\tcommentResp.ReplyUserDisplayName = replyUser.DisplayName\n\t\t\tcommentResp.ReplyUserStatus = replyUser.Status\n\t\t}\n\t}\n\n\t// check if current user vote this comment\n\tcommentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)\n\n\tcommentResp.MemberActions = permission.GetCommentPermission(ctx,\n\t\treq.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)\n\treturn commentResp, nil\n}\n\nfunc (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) {\n\tstatus := cs.voteCommon.GetVoteStatus(ctx, commentID, userID)\n\treturn len(status) > 0\n}\n\n// GetCommentPersonalWithPage get personal comment list page\nfunc (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *schema.GetCommentPersonalWithPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tif len(req.Username) > 0 {\n\t\tuserInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t\t}\n\t\treq.UserID = userInfo.ID\n\t}\n\tif len(req.UserID) == 0 {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tdto := &CommentQuery{\n\t\tPageCond:  pager.PageCond{Page: req.Page, PageSize: req.PageSize},\n\t\tUserID:    req.UserID,\n\t\tQueryCond: \"created_at\",\n\t}\n\tcommentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := make([]*schema.GetCommentPersonalWithPageResp, 0)\n\tfor _, comment := range commentList {\n\t\tcommentResp := &schema.GetCommentPersonalWithPageResp{\n\t\t\tCommentID: comment.ID,\n\t\t\tCreatedAt: comment.CreatedAt.Unix(),\n\t\t\tObjectID:  comment.ObjectID,\n\t\t\tContent:   comment.ParsedText, // todo trim\n\t\t}\n\t\tif len(comment.ObjectID) > 0 {\n\t\t\tobjInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t} else {\n\t\t\t\tcommentResp.ObjectType = objInfo.ObjectType\n\t\t\t\tcommentResp.Title = objInfo.Title\n\t\t\t\tcommentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)\n\t\t\t\tcommentResp.QuestionID = objInfo.QuestionID\n\t\t\t\tcommentResp.AnswerID = objInfo.AnswerID\n\t\t\t\tif objInfo.QuestionStatus == entity.QuestionStatusDeleted {\n\t\t\t\t\tcommentResp.Title = \"Deleted question\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tresp = append(resp, commentResp)\n\t}\n\treturn pager.NewPageModel(total, resp), nil\n}\n\nfunc (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID,\n\tquestionID, questionTitle, commentID, commentUserID, commentSummary string) {\n\tif questionUserID == commentUserID {\n\t\treturn\n\t}\n\t// send internal notification\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: questionUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationCommentQuestion\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// send external notification\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", questionUserID)\n\t\treturn\n\t}\n\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *CommentService) notificationAnswerComment(ctx context.Context,\n\tquestionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {\n\tif answerUserID == commentUserID {\n\t\treturn\n\t}\n\n\t// Send internal notification.\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: answerUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationCommentAnswer\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// Send external notification.\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", answerUserID)\n\t\treturn\n\t}\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tAnswerID:        answerID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID,\n\tquestionID, questionTitle, commentSummary string) {\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: replyUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationReplyToYou\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// Send external notification.\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", replyUserID)\n\t\treturn\n\t}\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *CommentService) notificationMention(\n\tctx context.Context, mentionUsernameList []string, commentID, commentUserID string,\n\talreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {\n\tfor _, username := range mentionUsernameList {\n\t\tuserInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tif exist && !alreadyNotifiedUserID[userInfo.ID] {\n\t\t\tmsg := &schema.NotificationMsg{\n\t\t\t\tReceiverUserID: userInfo.ID,\n\t\t\t\tTriggerUserID:  commentUserID,\n\t\t\t\tType:           schema.NotificationTypeInbox,\n\t\t\t\tObjectID:       commentID,\n\t\t\t}\n\t\t\tmsg.ObjectType = constant.CommentObjectType\n\t\t\tmsg.NotificationAction = constant.NotificationMentionYou\n\t\t\tcs.notificationQueueService.Send(ctx, msg)\n\t\t\talreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)\n\t\t}\n\t}\n\treturn alreadyNotifiedUserIDs\n}\n"
  },
  {
    "path": "internal/service/comment_common/comment_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage comment_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// CommentCommonRepo comment repository\ntype CommentCommonRepo interface {\n\tGetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)\n\tGetCommentWithoutStatus(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)\n\tGetCommentCount(ctx context.Context) (count int64, err error)\n\tRemoveAllUserComment(ctx context.Context, userID string) (err error)\n\tUpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)\n}\n\n// CommentCommonService user service\ntype CommentCommonService struct {\n\tcommentRepo CommentCommonRepo\n}\n\n// NewCommentCommonService new comment service\nfunc NewCommentCommonService(\n\tcommentRepo CommentCommonRepo) *CommentCommonService {\n\treturn &CommentCommonService{\n\t\tcommentRepo: commentRepo,\n\t}\n}\n\n// GetComment get comment one\nfunc (cs *CommentCommonService) GetComment(ctx context.Context, commentID string) (resp *schema.GetCommentResp, err error) {\n\tcomment, exist, err := cs.commentRepo.GetComment(ctx, commentID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.CommentNotFound)\n\t}\n\n\tresp = &schema.GetCommentResp{}\n\tresp.SetFromComment(comment)\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/config/config_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage config\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// ConfigRepo config repository\ntype ConfigRepo interface {\n\tGetConfigByID(ctx context.Context, id int) (c *entity.Config, err error)\n\tGetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error)\n\tGetConfigByKeyFromDB(ctx context.Context, key string) (c *entity.Config, err error)\n\tUpdateConfig(ctx context.Context, key, value string) (err error)\n}\n\n// ConfigService user service\ntype ConfigService struct {\n\tconfigRepo ConfigRepo\n}\n\n// NewConfigService new config service\nfunc NewConfigService(configRepo ConfigRepo) *ConfigService {\n\treturn &ConfigService{\n\t\tconfigRepo: configRepo,\n\t}\n}\n\n// GetIntValue get config int value\nfunc (cs *ConfigService) GetIntValue(ctx context.Context, key string) (val int, err error) {\n\tcf, err := cs.configRepo.GetConfigByKey(ctx, key)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn cf.GetIntValue(), nil\n}\n\n// GetStringValue get config string value\nfunc (cs *ConfigService) GetStringValue(ctx context.Context, key string) (val string, err error) {\n\tcf, err := cs.configRepo.GetConfigByKey(ctx, key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cf.Value, nil\n}\n\n// GetStringValueFromDB gets config string value directly from DB, bypassing cache.\nfunc (cs *ConfigService) GetStringValueFromDB(ctx context.Context, key string) (val string, err error) {\n\tcf, err := cs.configRepo.GetConfigByKeyFromDB(ctx, key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cf.Value, nil\n}\n\n// GetArrayStringValue get config array string value\nfunc (cs *ConfigService) GetArrayStringValue(ctx context.Context, key string) (val []string, err error) {\n\tcf, err := cs.configRepo.GetConfigByKey(ctx, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn cf.GetArrayStringValue(), nil\n}\n\nfunc (cs *ConfigService) GetJsonConfigByIDAndSetToObject(ctx context.Context, id int, obj any) (err error) {\n\tcf, err := cs.configRepo.GetConfigByID(ctx, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = json.Unmarshal([]byte(cf.Value), obj)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"[%s] config value is not json format\", cf.Key)\n\t}\n\treturn nil\n}\n\n// GetConfigByID get config by id\nfunc (cs *ConfigService) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) {\n\treturn cs.configRepo.GetConfigByID(ctx, id)\n}\n\nfunc (cs *ConfigService) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) {\n\treturn cs.configRepo.GetConfigByKey(ctx, key)\n}\n\n// GetIDByKey get config id by key\nfunc (cs *ConfigService) GetIDByKey(ctx context.Context, key string) (id int, err error) {\n\tcf, err := cs.configRepo.GetConfigByKey(ctx, key)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn cf.ID, nil\n}\n\nfunc (cs *ConfigService) UpdateConfig(ctx context.Context, key, value string) (err error) {\n\treturn cs.configRepo.UpdateConfig(ctx, key, value)\n}\n"
  },
  {
    "path": "internal/service/content/answer_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/role\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// AnswerService user service\ntype AnswerService struct {\n\tanswerRepo                       answercommon.AnswerRepo\n\tquestionRepo                     questioncommon.QuestionRepo\n\tquestionCommon                   *questioncommon.QuestionCommon\n\tanswerActivityService            *activity.AnswerActivityService\n\tuserCommon                       *usercommon.UserCommon\n\tcollectionCommon                 *collectioncommon.CollectionCommon\n\tuserRepo                         usercommon.UserRepo\n\trevisionService                  *revision_common.RevisionService\n\tAnswerCommon                     *answercommon.AnswerCommon\n\tvoteRepo                         activity_common.VoteRepo\n\temailService                     *export.EmailService\n\troleService                      *role.UserRoleRelService\n\tnotificationQueueService         noticequeue.Service\n\texternalNotificationQueueService noticequeue.ExternalService\n\tactivityQueueService             activityqueue.Service\n\treviewService                    *review.ReviewService\n\teventQueueService                eventqueue.Service\n}\n\nfunc NewAnswerService(\n\tanswerRepo answercommon.AnswerRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\tquestionCommon *questioncommon.QuestionCommon,\n\tuserCommon *usercommon.UserCommon,\n\tcollectionCommon *collectioncommon.CollectionCommon,\n\tuserRepo usercommon.UserRepo,\n\trevisionService *revision_common.RevisionService,\n\tanswerAcceptActivityRepo *activity.AnswerActivityService,\n\tanswerCommon *answercommon.AnswerCommon,\n\tvoteRepo activity_common.VoteRepo,\n\temailService *export.EmailService,\n\troleService *role.UserRoleRelService,\n\tnotificationQueueService noticequeue.Service,\n\texternalNotificationQueueService noticequeue.ExternalService,\n\tactivityQueueService activityqueue.Service,\n\treviewService *review.ReviewService,\n\teventQueueService eventqueue.Service,\n) *AnswerService {\n\treturn &AnswerService{\n\t\tanswerRepo:                       answerRepo,\n\t\tquestionRepo:                     questionRepo,\n\t\tuserCommon:                       userCommon,\n\t\tcollectionCommon:                 collectionCommon,\n\t\tquestionCommon:                   questionCommon,\n\t\tuserRepo:                         userRepo,\n\t\trevisionService:                  revisionService,\n\t\tanswerActivityService:            answerAcceptActivityRepo,\n\t\tAnswerCommon:                     answerCommon,\n\t\tvoteRepo:                         voteRepo,\n\t\temailService:                     emailService,\n\t\troleService:                      roleService,\n\t\tnotificationQueueService:         notificationQueueService,\n\t\texternalNotificationQueueService: externalNotificationQueueService,\n\t\tactivityQueueService:             activityQueueService,\n\t\treviewService:                    reviewService,\n\t\teventQueueService:                eventQueueService,\n\t}\n}\n\n// RemoveAnswer delete answer\nfunc (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAnswerReq) (err error) {\n\tanswerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\t// if the status is deleted, return directly\n\tif answerInfo.Status == entity.AnswerStatusDeleted {\n\t\treturn nil\n\t}\n\troleID, err := as.roleService.GetUserRole(ctx, req.UserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif roleID != role.RoleAdminID && roleID != role.RoleModeratorID {\n\t\tif answerInfo.UserID != req.UserID {\n\t\t\treturn errors.BadRequest(reason.AnswerCannotDeleted)\n\t\t}\n\t\tif answerInfo.VoteCount > 0 {\n\t\t\treturn errors.BadRequest(reason.AnswerCannotDeleted)\n\t\t}\n\t\tif answerInfo.Accepted == schema.AnswerAcceptedEnable {\n\t\t\treturn errors.BadRequest(reason.AnswerCannotDeleted)\n\t\t}\n\t\t_, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn errors.BadRequest(reason.AnswerCannotDeleted)\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.AnswerCannotDeleted)\n\t\t}\n\t}\n\n\terr = as.answerRepo.RemoveAnswer(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// user add question count\n\terr = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID)\n\tif err != nil {\n\t\tlog.Error(\"IncreaseAnswerCount error\", err.Error())\n\t}\n\tuserAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)\n\tif err != nil {\n\t\tlog.Error(\"GetCountByUserID error\", err.Error())\n\t}\n\terr = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))\n\tif err != nil {\n\t\tlog.Error(\"user IncreaseAnswerCount error\", err.Error())\n\t}\n\terr = as.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: answerInfo.QuestionID,\n\t\tFromAnswerID:   answerInfo.ID,\n\t}, &entity.QuestionLink{\n\t\tToQuestionID: answerInfo.QuestionID,\n\t\tToAnswerID:   answerInfo.ID,\n\t})\n\tif err != nil {\n\t\tlog.Error(\"RemoveQuestionLink error\", err.Error())\n\t}\n\n\t// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,\n\t// facing the problem of recovery.\n\t// err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)\n\t// if err != nil {\n\t// \tlog.Errorf(\"delete answer activity change failed: %s\", err.Error())\n\t// }\n\tas.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\tObjectID:         answerInfo.ID,\n\t\tOriginalObjectID: answerInfo.ID,\n\t\tActivityTypeKey:  constant.ActAnswerDeleted,\n\t})\n\tas.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID).\n\t\tAID(answerInfo.ID, answerInfo.UserID))\n\treturn\n}\n\n// RecoverAnswer recover deleted answer\nfunc (as *AnswerService) RecoverAnswer(ctx context.Context, req *schema.RecoverAnswerReq) (err error) {\n\tanswerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.AnswerNotFound)\n\t}\n\tif answerInfo.Status != entity.AnswerStatusDeleted {\n\t\treturn nil\n\t}\n\tif err = as.answerRepo.RecoverAnswer(ctx, req.AnswerID); err != nil {\n\t\treturn err\n\t}\n\tif err = as.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: answerInfo.QuestionID,\n\t\tFromAnswerID:   answerInfo.ID,\n\t}, &entity.QuestionLink{\n\t\tToQuestionID: answerInfo.QuestionID,\n\t\tToAnswerID:   answerInfo.ID,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil {\n\t\tlog.Errorf(\"update answer count failed: %s\", err.Error())\n\t}\n\tuserAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)\n\tif err != nil {\n\t\tlog.Errorf(\"get user answer count failed: %s\", err.Error())\n\t} else {\n\t\terr = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"update user answer count failed: %s\", err.Error())\n\t\t}\n\t}\n\tas.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\tObjectID:         answerInfo.ID,\n\t\tOriginalObjectID: answerInfo.ID,\n\t\tActivityTypeKey:  constant.ActAnswerUndeleted,\n\t})\n\treturn nil\n}\n\nfunc (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) {\n\tquestionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", errors.BadRequest(reason.QuestionNotFound)\n\t}\n\tif questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted {\n\t\terr = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion)\n\t\treturn \"\", err\n\t}\n\tinsertData := &entity.Answer{}\n\tinsertData.UserID = req.UserID\n\tinsertData.OriginalText = req.Content\n\tinsertData.ParsedText = req.HTML\n\tinsertData.Accepted = schema.AnswerAcceptedFailed\n\tinsertData.QuestionID = req.QuestionID\n\tinsertData.RevisionID = \"0\"\n\tinsertData.LastEditUserID = \"0\"\n\tinsertData.Status = entity.AnswerStatusPending\n\t// insertData.UpdatedAt = now\n\tif err = as.answerRepo.AddAnswer(ctx, insertData); err != nil {\n\t\treturn \"\", err\n\t}\n\tinsertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent)\n\tif err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil {\n\t\treturn \"\", err\n\t}\n\tif insertData.Status == entity.AnswerStatusAvailable {\n\t\tinsertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{\"parsed_text\"}); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\terr = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID)\n\tif err != nil {\n\t\tlog.Error(\"IncreaseAnswerCount error\", err.Error())\n\t}\n\terr = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID))\n\tif err != nil {\n\t\tlog.Error(\"UpdateLastAnswer error\", err.Error())\n\t}\n\terr = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn insertData.ID, err\n\t}\n\tuserAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\tlog.Error(\"GetCountByUserID error\", err.Error())\n\t}\n\terr = as.userCommon.UpdateAnswerCount(ctx, req.UserID, int(userAnswerCount))\n\tif err != nil {\n\t\tlog.Error(\"user IncreaseAnswerCount error\", err.Error())\n\t}\n\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   insertData.UserID,\n\t\tObjectID: insertData.ID,\n\t\tTitle:    \"\",\n\t}\n\tinfoJSON, _ := json.Marshal(insertData)\n\trevisionDTO.Content = string(infoJSON)\n\trevisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn insertData.ID, err\n\t}\n\tif insertData.Status == entity.AnswerStatusAvailable {\n\t\tas.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,\n\t\t\thtmltext.FetchExcerpt(insertData.ParsedText, \"...\", 240))\n\t}\n\n\tas.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           insertData.UserID,\n\t\tObjectID:         insertData.ID,\n\t\tOriginalObjectID: insertData.ID,\n\t\tActivityTypeKey:  constant.ActAnswerAnswered,\n\t\tRevisionID:       revisionID,\n\t})\n\tas.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           insertData.UserID,\n\t\tObjectID:         insertData.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionAnswered,\n\t})\n\tas.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID).\n\t\tAID(insertData.ID, insertData.UserID))\n\treturn insertData.ID, nil\n}\n\nfunc (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) {\n\tvar canUpdate bool\n\t_, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif existUnreviewed {\n\t\treturn \"\", errors.BadRequest(reason.AnswerCannotUpdate)\n\t}\n\n\tanswerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", errors.BadRequest(reason.AnswerNotFound)\n\t}\n\tif answerInfo.Status == entity.AnswerStatusDeleted {\n\t\treturn \"\", errors.BadRequest(reason.AnswerCannotUpdate)\n\t}\n\n\tquestionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", errors.BadRequest(reason.QuestionNotFound)\n\t}\n\n\t// If the content is the same, ignore it\n\tif answerInfo.OriginalText == req.Content {\n\t\treturn \"\", nil\n\t}\n\n\tinsertData := &entity.Answer{}\n\tinsertData.ID = req.ID\n\tinsertData.UserID = answerInfo.UserID\n\tinsertData.QuestionID = questionInfo.ID\n\tinsertData.OriginalText = req.Content\n\tinsertData.ParsedText = req.HTML\n\tinsertData.UpdatedAt = time.Now()\n\tinsertData.LastEditUserID = \"0\"\n\tif answerInfo.UserID != req.UserID {\n\t\tinsertData.LastEditUserID = req.UserID\n\t}\n\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   req.UserID,\n\t\tObjectID: req.ID,\n\t\tLog:      req.EditSummary,\n\t}\n\n\tif req.NoNeedReview || answerInfo.UserID == req.UserID {\n\t\tcanUpdate = true\n\t}\n\n\tif !canUpdate {\n\t\trevisionDTO.Status = entity.RevisionUnreviewedStatus\n\t} else {\n\t\tinsertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{\"original_text\", \"parsed_text\", \"updated_at\", \"last_edit_user_id\"}); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\terr = as.questionCommon.UpdatePostTime(ctx, questionInfo.ID)\n\t\tif err != nil {\n\t\t\treturn insertData.ID, err\n\t\t}\n\t\tas.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)\n\t\trevisionDTO.Status = entity.RevisionReviewPassStatus\n\t}\n\n\tinfoJSON, _ := json.Marshal(insertData)\n\trevisionDTO.Content = string(infoJSON)\n\trevisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn insertData.ID, err\n\t}\n\tif canUpdate {\n\t\tas.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           req.UserID,\n\t\t\tObjectID:         insertData.ID,\n\t\t\tOriginalObjectID: insertData.ID,\n\t\t\tActivityTypeKey:  constant.ActAnswerEdited,\n\t\t\tRevisionID:       revisionID,\n\t\t})\n\t\tas.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID).\n\t\t\tAID(insertData.ID, insertData.UserID))\n\t}\n\n\treturn insertData.ID, nil\n}\n\n// AcceptAnswer accept answer\nfunc (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAnswerReq) (err error) {\n\t// find question\n\tquestionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.QuestionNotFound)\n\t}\n\tquestionInfo.ID = uid.DeShortID(questionInfo.ID)\n\tif questionInfo.AcceptedAnswerID == req.AnswerID {\n\t\treturn nil\n\t}\n\n\t// find answer\n\tvar acceptedAnswerInfo *entity.Answer\n\tif len(req.AnswerID) > 1 {\n\t\tacceptedAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, req.AnswerID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.AnswerNotFound)\n\t\t}\n\n\t\t// check answer belong to question\n\t\tif acceptedAnswerInfo.QuestionID != req.QuestionID {\n\t\t\treturn errors.BadRequest(reason.AnswerNotFound)\n\t\t}\n\t\tacceptedAnswerInfo.ID = uid.DeShortID(acceptedAnswerInfo.ID)\n\t}\n\n\t// update answers status\n\tif err = as.answerRepo.UpdateAcceptedStatus(ctx, req.AnswerID, req.QuestionID); err != nil {\n\t\treturn err\n\t}\n\n\t// update question status\n\terr = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID)\n\tif err != nil {\n\t\tlog.Error(\"UpdateLastAnswer error\", err.Error())\n\t}\n\n\tvar oldAnswerInfo *entity.Answer\n\tif len(questionInfo.AcceptedAnswerID) > 1 {\n\t\toldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\toldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID)\n\t}\n\n\tif acceptedAnswerInfo != nil {\n\t\tas.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID).\n\t\t\tQID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID))\n\t}\n\n\tas.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo)\n\treturn nil\n}\n\nfunc (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,\n\tquestionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer,\n) {\n\t// if this question is already been answered, should cancel old answer rank\n\tif oldAnswerInfo != nil {\n\t\terr := as.answerActivityService.CancelAcceptAnswer(ctx, userID,\n\t\t\tquestionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\tif newAnswerInfo != nil {\n\t\terr := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID,\n\t\t\tquestionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == questionInfo.UserID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n}\n\nfunc (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) {\n\tanswerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)\n\tif err != nil {\n\t\treturn nil, nil, has, err\n\t}\n\tinfo := as.ShowFormat(ctx, answerInfo)\n\t// todo questionFunc\n\tquestionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID)\n\tif err != nil {\n\t\treturn nil, nil, has, err\n\t}\n\t// todo UserFunc\n\n\tuserIds := make([]string, 0)\n\tuserIds = append(userIds, answerInfo.UserID)\n\tuserIds = append(userIds, answerInfo.LastEditUserID)\n\tuserInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds)\n\tif err != nil {\n\t\treturn nil, nil, has, err\n\t}\n\n\t_, ok := userInfoMap[answerInfo.UserID]\n\tif ok {\n\t\tinfo.UserInfo = userInfoMap[answerInfo.UserID]\n\t}\n\t_, ok = userInfoMap[answerInfo.LastEditUserID]\n\tif ok {\n\t\tinfo.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID]\n\t}\n\n\tif loginUserID == \"\" {\n\t\treturn info, questionInfo, has, nil\n\t}\n\n\tinfo.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID)\n\n\tcollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID})\n\tif err != nil {\n\t\treturn nil, nil, has, err\n\t}\n\tif len(collectedMap) > 0 {\n\t\tinfo.Collected = true\n\t}\n\n\treturn info, questionInfo, has, nil\n}\n\nfunc (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) {\n\tanswerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !has {\n\t\treturn nil, errors.BadRequest(reason.AnswerNotFound)\n\t}\n\tinfo := as.ShowFormat(ctx, answerInfo)\n\treturn info, nil\n}\n\nfunc (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) {\n\treturn as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId)\n}\n\nfunc (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminUpdateAnswerStatusReq) error {\n\tsetStatus, ok := entity.AdminAnswerSearchStatus[req.Status]\n\tif !ok {\n\t\treturn errors.BadRequest(reason.RequestFormatError)\n\t}\n\tanswerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.AnswerNotFound)\n\t}\n\n\tif setStatus == entity.AnswerStatusDeleted {\n\t\tif err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{\n\t\t\tID:        req.AnswerID,\n\t\t\tUserID:    req.UserID,\n\t\t\tCanDelete: true,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmsg := &schema.NotificationMsg{}\n\t\tmsg.ObjectID = answerInfo.ID\n\t\tmsg.Type = schema.NotificationTypeInbox\n\t\tmsg.ReceiverUserID = answerInfo.UserID\n\t\tmsg.TriggerUserID = answerInfo.UserID\n\t\tmsg.ObjectType = constant.AnswerObjectType\n\t\tmsg.NotificationAction = constant.NotificationYourAnswerWasDeleted\n\t\tas.notificationQueueService.Send(ctx, msg)\n\t}\n\n\t// recover\n\tif setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted {\n\t\tif err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{\n\t\t\tAnswerID: req.AnswerID,\n\t\t\tUserID:   req.UserID,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {\n\tlist := make([]*schema.AnswerInfo, 0)\n\tdbSearch := entity.AnswerSearch{}\n\tdbSearch.QuestionID = req.QuestionID\n\tdbSearch.Page = req.Page\n\tdbSearch.PageSize = req.PageSize\n\tdbSearch.Order = req.Order\n\tdbSearch.IncludeDeleted = req.CanDelete\n\tdbSearch.LoginUserID = req.UserID\n\tanswerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)\n\tif err != nil {\n\t\treturn list, count, err\n\t}\n\tanswerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req)\n\tif err != nil {\n\t\treturn answerList, count, err\n\t}\n\treturn answerList, count, nil\n}\n\nfunc (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) (\n\t[]*schema.AnswerInfo, error) {\n\tlist := make([]*schema.AnswerInfo, 0)\n\tobjectIDs := make([]string, 0)\n\tuserIDs := make([]string, 0)\n\tfor _, info := range answers {\n\t\titem := as.ShowFormat(ctx, info)\n\t\tlist = append(list, item)\n\t\tobjectIDs = append(objectIDs, info.ID)\n\t\tuserIDs = append(userIDs, info.UserID, info.LastEditUserID)\n\t}\n\n\tuserInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)\n\tif err != nil {\n\t\treturn list, err\n\t}\n\tfor _, item := range list {\n\t\titem.UserInfo = userInfoMap[item.UserID]\n\t\titem.UpdateUserInfo = userInfoMap[item.UpdateUserID]\n\t}\n\tif len(req.UserID) == 0 {\n\t\treturn list, nil\n\t}\n\n\tcollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, item := range list {\n\t\titem.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID)\n\t\titem.Collected = collectedMap[item.ID]\n\t\titem.MemberActions = permission.GetAnswerPermission(ctx,\n\t\t\treq.UserID,\n\t\t\titem.UserID,\n\t\t\titem.Status,\n\t\t\treq.CanEdit,\n\t\t\treq.CanDelete,\n\t\t\treq.CanRecover)\n\t}\n\treturn list, nil\n}\n\nfunc (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo {\n\treturn as.AnswerCommon.ShowFormat(ctx, data)\n}\n\nfunc (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) {\n\t// If the answer is updated by me, there is no notification for myself.\n\t// equivalent behaviour as AnswerService.notificationAnswerTheQuestion\n\tif questionUserID == answerUserID {\n\t\treturn\n\t}\n\tmsg := &schema.NotificationMsg{\n\t\tTriggerUserID:  answerUserID,\n\t\tReceiverUserID: questionUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       answerID,\n\t}\n\tmsg.ObjectType = constant.AnswerObjectType\n\tmsg.NotificationAction = constant.NotificationUpdateAnswer\n\tas.notificationQueueService.Send(ctx, msg)\n}\n\nfunc (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,\n\tquestionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {\n\t// If the question is answered by me, there is no notification for myself.\n\tif questionUserID == answerUserID {\n\t\treturn\n\t}\n\tmsg := &schema.NotificationMsg{\n\t\tTriggerUserID:  answerUserID,\n\t\tReceiverUserID: questionUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       answerID,\n\t}\n\tmsg.ObjectType = constant.AnswerObjectType\n\tmsg.NotificationAction = constant.NotificationAnswerTheQuestion\n\tas.notificationQueueService.Send(ctx, msg)\n\n\treceiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", questionUserID)\n\t\treturn\n\t}\n\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewAnswerTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tAnswerID:        answerID,\n\t\tAnswerSummary:   answerSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tanswerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)\n\tif answerUser != nil {\n\t\trawData.AnswerUserDisplayName = answerUser.DisplayName\n\t}\n\texternalNotificationMsg.NewAnswerTemplateRawData = rawData\n\tas.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n"
  },
  {
    "path": "internal/service/content/question_hottest_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc (q *QuestionService) RefreshHottestCron(ctx context.Context) {\n\tvar (\n\t\tpage     = 1\n\t\tpageSize = 100\n\t)\n\n\tfor {\n\t\tquestionList, _, err := q.questionRepo.GetQuestionPage(\n\t\t\tctx,\n\t\t\tpage, pageSize,\n\t\t\t[]string{},\n\t\t\t\"\", \"newest\",\n\t\t\tschema.HotInDays,\n\t\t\tfalse, false)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tfor _, question := range questionList {\n\t\t\tupdatedAt := question.UpdatedAt.Unix()\n\t\t\tif updatedAt < 0 {\n\t\t\t\tupdatedAt = question.CreatedAt.Unix()\n\t\t\t}\n\n\t\t\tqAgeInHours := (time.Now().Unix() - question.CreatedAt.Unix()) / 3600\n\t\t\tqUpdated := (time.Now().Unix() - updatedAt) / 3600\n\n\t\t\taScores, err := q.answerRepo.SumVotesByQuestionID(ctx, question.ID)\n\t\t\tif err != nil {\n\t\t\t\taScores = 0\n\t\t\t}\n\n\t\t\tscore := q.getScore(float64(question.ViewCount), float64(question.AnswerCount), float64(question.VoteCount), aScores, float64(qAgeInHours), float64(qUpdated))\n\t\t\tif score < 0 {\n\t\t\t\tscore = 0\n\t\t\t}\n\n\t\t\tquestioninfo := &entity.Question{}\n\t\t\tquestioninfo.ID = question.ID\n\t\t\tquestioninfo.HotScore = int(math.Ceil(score * 10000))\n\t\t\terr = q.questionRepo.UpdateQuestion(ctx, questioninfo, []string{\"hot_score\"})\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"update question hot score error,question ID:\", question.ID, \" error: \", err)\n\t\t\t}\n\t\t}\n\n\t\tif len(questionList) < pageSize {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\n}\n\nfunc (q *QuestionService) getScore(qViews, qAnswers, qScore, aScores, qAgeInHours, qUpdated float64) (score float64) {\n\tscore = ((math.Log(qViews) * 4) + ((qAnswers * qScore) / 5) + aScores) /\n\t\tmath.Pow(((qAgeInHours+1)-((qAgeInHours-qUpdated)/2)), 1.5)\n\treturn score\n}\n"
  },
  {
    "path": "internal/service/content/question_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/apache/answer/plugin\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/export\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/notification\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/tag\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/net/context\"\n)\n\n// QuestionRepo question repository\n\n// QuestionService user service\ntype QuestionService struct {\n\tactivityRepo                     activity_common.ActivityRepo\n\tquestionRepo                     questioncommon.QuestionRepo\n\tanswerRepo                       answercommon.AnswerRepo\n\ttagCommon                        *tagcommon.TagCommonService\n\ttagService                       *tag.TagService\n\tquestioncommon                   *questioncommon.QuestionCommon\n\tuserCommon                       *usercommon.UserCommon\n\tuserRepo                         usercommon.UserRepo\n\tuserRoleRelService               *role.UserRoleRelService\n\trevisionService                  *revision_common.RevisionService\n\tmetaService                      *metacommon.MetaCommonService\n\tcollectionCommon                 *collectioncommon.CollectionCommon\n\tanswerActivityService            *activity.AnswerActivityService\n\temailService                     *export.EmailService\n\tnotificationQueueService         noticequeue.Service\n\texternalNotificationQueueService noticequeue.ExternalService\n\tactivityQueueService             activityqueue.Service\n\tsiteInfoService                  siteinfo_common.SiteInfoCommonService\n\tnewQuestionNotificationService   *notification.ExternalNotificationService\n\treviewService                    *review.ReviewService\n\tconfigService                    *config.ConfigService\n\teventQueueService                eventqueue.Service\n\treviewRepo                       review.ReviewRepo\n}\n\nfunc NewQuestionService(\n\tactivityRepo activity_common.ActivityRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\ttagCommon *tagcommon.TagCommonService,\n\ttagService *tag.TagService,\n\tquestioncommon *questioncommon.QuestionCommon,\n\tuserCommon *usercommon.UserCommon,\n\tuserRepo usercommon.UserRepo,\n\tuserRoleRelService *role.UserRoleRelService,\n\trevisionService *revision_common.RevisionService,\n\tmetaService *metacommon.MetaCommonService,\n\tcollectionCommon *collectioncommon.CollectionCommon,\n\tanswerActivityService *activity.AnswerActivityService,\n\temailService *export.EmailService,\n\tnotificationQueueService noticequeue.Service,\n\texternalNotificationQueueService noticequeue.ExternalService,\n\tactivityQueueService activityqueue.Service,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tnewQuestionNotificationService *notification.ExternalNotificationService,\n\treviewService *review.ReviewService,\n\tconfigService *config.ConfigService,\n\teventQueueService eventqueue.Service,\n\treviewRepo review.ReviewRepo,\n) *QuestionService {\n\treturn &QuestionService{\n\t\tactivityRepo:                     activityRepo,\n\t\tquestionRepo:                     questionRepo,\n\t\tanswerRepo:                       answerRepo,\n\t\ttagCommon:                        tagCommon,\n\t\ttagService:                       tagService,\n\t\tquestioncommon:                   questioncommon,\n\t\tuserCommon:                       userCommon,\n\t\tuserRepo:                         userRepo,\n\t\tuserRoleRelService:               userRoleRelService,\n\t\trevisionService:                  revisionService,\n\t\tmetaService:                      metaService,\n\t\tcollectionCommon:                 collectionCommon,\n\t\tanswerActivityService:            answerActivityService,\n\t\temailService:                     emailService,\n\t\tnotificationQueueService:         notificationQueueService,\n\t\texternalNotificationQueueService: externalNotificationQueueService,\n\t\tactivityQueueService:             activityQueueService,\n\t\tsiteInfoService:                  siteInfoService,\n\t\tnewQuestionNotificationService:   newQuestionNotificationService,\n\t\treviewService:                    reviewService,\n\t\tconfigService:                    configService,\n\t\teventQueueService:                eventQueueService,\n\t\treviewRepo:                       reviewRepo,\n\t}\n}\n\nfunc (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\n\tcf, err := qs.configService.GetConfigByID(ctx, req.CloseType)\n\tif err != nil || cf == nil {\n\t\treturn errors.BadRequest(reason.ReportNotFound)\n\t}\n\tif cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.CloseMsg) {\n\t\treturn errors.BadRequest(reason.InvalidURLError)\n\t}\n\n\tquestionInfo.Status = entity.QuestionStatusClosed\n\terr = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcloseMeta, _ := json.Marshal(schema.CloseQuestionMeta{\n\t\tCloseType: req.CloseType,\n\t\tCloseMsg:  req.CloseMsg,\n\t})\n\terr = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif cf.Key == constant.ReasonADuplicate {\n\t\tqs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg)\n\t}\n\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tObjectID:         questionInfo.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionClosed,\n\t})\n\treturn nil\n}\n\n// ReopenQuestion reopen question\nfunc (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.ReopenQuestionReq) error {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\n\tquestionInfo.Status = entity.QuestionStatusAvailable\n\terr = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)\n\tif err != nil {\n\t\treturn err\n\t}\n\tqs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo)\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tObjectID:         questionInfo.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionReopened,\n\t})\n\treturn nil\n}\n\nfunc (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, tags []*entity.Tag) ([]string, error) {\n\tlist := make([]string, 0)\n\tfor _, tag := range tags {\n\t\tif tag.Reserved {\n\t\t\tlist = append(list, tag.DisplayName)\n\t\t}\n\t}\n\tif len(list) > 0 {\n\t\treturn list, errors.BadRequest(reason.RequestFormatError)\n\t}\n\treturn []string{}, nil\n}\nfunc (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) {\n\tminimumTags, err := qs.tagCommon.GetMinimumTags(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(req.Tags) < minimumTags {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),\n\t\t})\n\t\terr = errors.BadRequest(reason.TagMinCount)\n\t\treturn errorlist, err\n\t}\n\tminimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(req.Content) < minimumContentLength {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"content\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),\n\t\t})\n\t\terr = errors.BadRequest(reason.QuestionContentLessThanMinimum)\n\t\treturn errorlist, err\n\t}\n\trecommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !recommendExist {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),\n\t\t})\n\t\terr = errors.BadRequest(reason.RecommendTagEnter)\n\t\treturn errorlist, err\n\t}\n\n\ttagNameList := make([]string, 0)\n\tfor _, tag := range req.Tags {\n\t\ttagNameList = append(tagNameList, tag.SlugName)\n\t}\n\tTags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)\n\tif tagerr != nil {\n\t\treturn errorlist, tagerr\n\t}\n\tif !req.CanUseReservedTag {\n\t\ttaglist, err := qs.AddQuestionCheckTags(ctx, Tags)\n\t\terrMsg := fmt.Sprintf(`\"%s\" can only be used by moderators.`,\n\t\t\tstrings.Join(taglist, \",\"))\n\t\tif err != nil {\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RecommendTagEnter)\n\t\t\treturn errorlist, err\n\t\t}\n\t}\n\treturn nil, nil\n}\n\n// HasNewTag\nfunc (qs *QuestionService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) {\n\treturn qs.tagCommon.HasNewTag(ctx, tags)\n}\n\n// AddQuestion add question\nfunc (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {\n\tminimumTags, err := qs.tagCommon.GetMinimumTags(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(req.Tags) < minimumTags {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),\n\t\t})\n\t\terr = errors.BadRequest(reason.TagMinCount)\n\t\treturn errorlist, err\n\t}\n\tminimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(req.Content) < minimumContentLength {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"content\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),\n\t\t})\n\t\terr = errors.BadRequest(reason.QuestionContentLessThanMinimum)\n\t\treturn errorlist, err\n\t}\n\trecommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !recommendExist {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),\n\t\t})\n\t\terr = errors.BadRequest(reason.RecommendTagEnter)\n\t\treturn errorlist, err\n\t}\n\n\ttagNameList := make([]string, 0)\n\tfor _, tag := range req.Tags {\n\t\ttag.SlugName = strings.ReplaceAll(tag.SlugName, \" \", \"-\")\n\t\ttagNameList = append(tagNameList, tag.SlugName)\n\t}\n\ttags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)\n\tif tagerr != nil {\n\t\treturn questionInfo, tagerr\n\t}\n\tif !req.CanUseReservedTag {\n\t\ttaglist, err := qs.AddQuestionCheckTags(ctx, tags)\n\t\terrMsg := fmt.Sprintf(`\"%s\" can only be used by moderators.`,\n\t\t\tstrings.Join(taglist, \",\"))\n\t\tif err != nil {\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RecommendTagEnter)\n\t\t\treturn errorlist, err\n\t\t}\n\t}\n\n\tquestion := &entity.Question{}\n\tnow := time.Now()\n\tquestion.UserID = req.UserID\n\tquestion.Title = req.Title\n\tquestion.OriginalText = req.Content\n\tquestion.ParsedText = req.HTML\n\tquestion.AcceptedAnswerID = \"0\"\n\tquestion.LastAnswerID = \"0\"\n\tquestion.LastEditUserID = \"0\"\n\t// question.PostUpdateTime = nil\n\tquestion.Status = entity.QuestionStatusPending\n\tquestion.RevisionID = \"0\"\n\tquestion.CreatedAt = now\n\tquestion.PostUpdateTime = now\n\tquestion.Pin = entity.QuestionUnPin\n\tquestion.Show = entity.QuestionShow\n\t// question.UpdatedAt = nil\n\terr = qs.questionRepo.AddQuestion(ctx, question)\n\tif err != nil {\n\t\treturn\n\t}\n\tquestion.Status = qs.reviewService.AddQuestionReview(ctx, question, req.Tags, req.IP, req.UserAgent)\n\tif err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, question.Status); err != nil {\n\t\treturn nil, err\n\t}\n\tif question.Status == entity.QuestionStatusAvailable {\n\t\tquestion.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, \"\", question.ParsedText, question.OriginalText)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = qs.questionRepo.UpdateQuestion(ctx, question, []string{\"parsed_text\"})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tobjectTagData := schema.TagChange{}\n\tobjectTagData.ObjectID = question.ID\n\tobjectTagData.Tags = req.Tags\n\tobjectTagData.UserID = req.UserID\n\terrorlist, err := qs.ChangeTag(ctx, &objectTagData)\n\tif err != nil {\n\t\treturn errorlist, err\n\t}\n\t_ = qs.questionRepo.UpdateSearch(ctx, question.ID)\n\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   question.UserID,\n\t\tObjectID: question.ID,\n\t\tTitle:    question.Title,\n\t}\n\n\tquestionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, tags)\n\tinfoJSON, _ := json.Marshal(questionWithTagsRevision)\n\trevisionDTO.Content = string(infoJSON)\n\trevisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// user add question count\n\tuserQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID)\n\tif err != nil {\n\t\tlog.Errorf(\"get user question count error %v\", err)\n\t} else {\n\t\terr = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"update user question count error %v\", err)\n\t\t}\n\t}\n\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           question.UserID,\n\t\tObjectID:         question.ID,\n\t\tOriginalObjectID: question.ID,\n\t\tActivityTypeKey:  constant.ActQuestionAsked,\n\t\tRevisionID:       revisionID,\n\t})\n\n\tif question.Status == entity.QuestionStatusAvailable {\n\t\tnewTags, newTagsErr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)\n\t\tif newTagsErr != nil {\n\t\t\tlog.Error(\"get question newTags error %v\", newTagsErr)\n\t\t\tqs.externalNotificationQueueService.Send(ctx,\n\t\t\t\tschema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags))\n\t\t} else {\n\t\t\tqs.externalNotificationQueueService.Send(ctx,\n\t\t\t\tschema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, newTags))\n\t\t}\n\t}\n\tqs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID).\n\t\tQID(question.ID, question.UserID))\n\n\tquestionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)\n\treturn\n}\n\n// OperationQuestion\nfunc (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\t// Hidden question cannot be placed at the top\n\tif questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin {\n\t\treturn nil\n\t}\n\t// Question cannot be hidden when they are at the top\n\tif questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide {\n\t\treturn nil\n\t}\n\n\tswitch req.Operation {\n\tcase schema.QuestionOperationHide:\n\t\tquestionInfo.Show = entity.QuestionHide\n\t\terr = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{\n\t\t\tFromQuestionID: questionInfo.ID,\n\t\t}, &entity.QuestionLink{\n\t\t\tToQuestionID: questionInfo.ID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase schema.QuestionOperationShow:\n\t\tquestionInfo.Show = entity.QuestionShow\n\t\terr = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{\n\t\t\tFromQuestionID: questionInfo.ID,\n\t\t}, &entity.QuestionLink{\n\t\t\tToQuestionID: questionInfo.ID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase schema.QuestionOperationPin:\n\t\tquestionInfo.Pin = entity.QuestionPin\n\tcase schema.QuestionOperationUnPin:\n\t\tquestionInfo.Pin = entity.QuestionUnPin\n\t}\n\n\terr = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tactMap := make(map[string]constant.ActivityTypeKey)\n\tactMap[schema.QuestionOperationPin] = constant.ActQuestionPin\n\tactMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin\n\tactMap[schema.QuestionOperationHide] = constant.ActQuestionHide\n\tactMap[schema.QuestionOperationShow] = constant.ActQuestionShow\n\t_, ok := actMap[req.Operation]\n\tif ok {\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           req.UserID,\n\t\t\tObjectID:         questionInfo.ID,\n\t\t\tOriginalObjectID: questionInfo.ID,\n\t\t\tActivityTypeKey:  actMap[req.Operation],\n\t\t})\n\t}\n\n\treturn nil\n}\n\n// RemoveQuestion delete question\nfunc (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// if the status is deleted, return directly\n\tif questionInfo.Status == entity.QuestionStatusDeleted {\n\t\treturn nil\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\tif !req.IsAdmin {\n\t\tif questionInfo.UserID != req.UserID {\n\t\t\treturn errors.BadRequest(reason.QuestionCannotDeleted)\n\t\t}\n\n\t\tif questionInfo.AcceptedAnswerID != \"0\" {\n\t\t\treturn errors.BadRequest(reason.QuestionCannotDeleted)\n\t\t}\n\t\tif questionInfo.AnswerCount > 1 {\n\t\t\treturn errors.BadRequest(reason.QuestionCannotDeleted)\n\t\t}\n\n\t\tif questionInfo.AnswerCount == 1 {\n\t\t\tanswersearch := &entity.AnswerSearch{}\n\t\t\tanswersearch.QuestionID = req.ID\n\t\t\tanswerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, answer := range answerList {\n\t\t\t\tif answer.VoteCount > 0 {\n\t\t\t\t\treturn errors.BadRequest(reason.QuestionCannotDeleted)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tquestionInfo.Status = entity.QuestionStatusDeleted\n\terr = qs.questionRepo.UpdateQuestionStatusWithOutUpdateTime(ctx, questionInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)\n\tif err != nil {\n\t\tlog.Error(\"user GetUserQuestionCount error\", err.Error())\n\t} else {\n\t\terr = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)\n\t\tif err != nil {\n\t\t\tlog.Error(\"user IncreaseQuestionCount error\", err.Error())\n\t\t}\n\t}\n\n\t// If this question has been reviewed, then delete the review.\n\treviewInfo, exist, err := qs.reviewRepo.GetReviewByObject(ctx, questionInfo.ID)\n\tif exist && err == nil {\n\t\terr = qs.reviewRepo.UpdateReviewStatus(ctx, reviewInfo.ID, req.UserID, entity.ReviewStatusRejected)\n\t\tif err != nil {\n\t\t\treturn errors.InternalServer(reason.DatabaseError)\n\t\t}\n\t}\n\n\t// tag count\n\ttagIDs := make([]string, 0)\n\tTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)\n\tif tagerr != nil {\n\t\tlog.Error(\"GetObjectEntityTag error\", tagerr)\n\t\treturn nil\n\t}\n\tfor _, v := range Tags {\n\t\ttagIDs = append(tagIDs, v.ID)\n\t}\n\terr = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID)\n\tif err != nil {\n\t\tlog.Error(\"RemoveTagRelListByObjectID error\", err.Error())\n\t}\n\terr = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs)\n\tif err != nil {\n\t\tlog.Error(\"efreshTagQuestionCount error\", err.Error())\n\t}\n\n\t// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,\n\t// facing the problem of recovery.\n\t// err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)\n\t// if err != nil {\n\t// \t log.Errorf(\"user DeleteQuestion rank rollback error %s\", err.Error())\n\t// }\n\terr = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: questionInfo.ID,\n\t}, &entity.QuestionLink{\n\t\tToQuestionID: questionInfo.ID,\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           questionInfo.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\tObjectID:         questionInfo.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionDeleted,\n\t})\n\tqs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID).TID(questionInfo.ID).\n\t\tQID(questionInfo.ID, questionInfo.UserID))\n\treturn nil\n}\n\nfunc (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) {\n\tdbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !has {\n\t\treturn\n\t}\n\n\toldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)\n\tif tagerr != nil {\n\t\tlog.Error(\"GetObjectEntityTag error\", tagerr)\n\t\treturn nil, nil\n\t}\n\n\ttagNameList := make([]string, 0)\n\toldtagNameList := make([]string, 0)\n\tfor _, tag := range req.Tags {\n\t\ttagNameList = append(tagNameList, tag.SlugName)\n\t}\n\tfor _, tag := range oldTags {\n\t\toldtagNameList = append(oldtagNameList, tag.SlugName)\n\t}\n\n\tisChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)\n\n\t// If the content is the same, ignore it\n\tif dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {\n\t\treturn\n\t}\n\n\tTags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)\n\tif tagerr != nil {\n\t\tlog.Error(\"GetTagListByNames error\", tagerr)\n\t\treturn nil, nil\n\t}\n\n\t// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.\n\tif !req.CanUseReservedTag {\n\t\tCheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)\n\t\tif !CheckOldTag {\n\t\t\terrMsg := fmt.Sprintf(`The reserved tag \"%s\" must be present.`,\n\t\t\t\tstrings.Join(CheckOldTaglist, \",\"))\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t\t\treturn errorlist, err\n\t\t}\n\t\tif !CheckNewTag {\n\t\t\terrMsg := fmt.Sprintf(`\"%s\" can only be used by moderators.`,\n\t\t\t\tstrings.Join(CheckNewTaglist, \",\"))\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t\t\treturn errorlist, err\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (qs *QuestionService) RecoverQuestion(ctx context.Context, req *schema.QuestionRecoverReq) (err error) {\n\tquestionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.QuestionNotFound)\n\t}\n\tif questionInfo.Status != entity.QuestionStatusDeleted {\n\t\treturn nil\n\t}\n\n\terr = qs.questionRepo.RecoverQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// update user's question count\n\tuserQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)\n\tif err != nil {\n\t\tlog.Error(\"user GetUserQuestionCount error\", err.Error())\n\t} else {\n\t\terr = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)\n\t\tif err != nil {\n\t\t\tlog.Error(\"user IncreaseQuestionCount error\", err.Error())\n\t\t}\n\t}\n\n\t// update tag's question count\n\tif err = qs.tagCommon.RecoverTagRelListByObjectID(ctx, questionInfo.ID); err != nil {\n\t\tlog.Errorf(\"remove tag rel list by object id error %v\", err)\n\t}\n\n\ttagIDs := make([]string, 0)\n\ttags, err := qs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, v := range tags {\n\t\ttagIDs = append(tagIDs, v.ID)\n\t}\n\tif len(tagIDs) > 0 {\n\t\tif err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs); err != nil {\n\t\t\tlog.Errorf(\"update tag's question count failed, %v\", err)\n\t\t}\n\t}\n\terr = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: questionInfo.ID,\n\t}, &entity.QuestionLink{\n\t\tToQuestionID: questionInfo.ID,\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\tObjectID:         questionInfo.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionUndeleted,\n\t})\n\treturn nil\n}\n\nfunc (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) {\n\toriginQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.QuestionNotFound)\n\t}\n\n\t// verify invite user\n\tinviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser)\n\tif err != nil {\n\t\tlog.Error(\"BatchGetUserBasicInfoByUserNames error\", err.Error())\n\t}\n\tinviteUserIDs := make([]string, 0)\n\tfor _, item := range req.InviteUser {\n\t\t_, ok := inviteUserInfoList[item]\n\t\tif ok {\n\t\t\tinviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID)\n\t\t}\n\t}\n\tinviteUserStr := \"\"\n\tinviteUserByte, err := json.Marshal(inviteUserIDs)\n\tif err != nil {\n\t\tlog.Error(\"json.Marshal error\", err.Error())\n\t\tinviteUserStr = \"[]\"\n\t} else {\n\t\tinviteUserStr = string(inviteUserByte)\n\t}\n\tquestion := &entity.Question{}\n\tquestion.ID = uid.DeShortID(req.ID)\n\tquestion.InviteUserID = inviteUserStr\n\n\tsaveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{\"invite_user_id\"})\n\tif saveerr != nil {\n\t\treturn saveerr\n\t}\n\t// send notification\n\toldInviteUserIDsStr := originQuestion.InviteUserID\n\toldInviteUserIDs := make([]string, 0)\n\tneedSendNotificationUserIDs := make([]string, 0)\n\tif oldInviteUserIDsStr != \"\" {\n\t\terr = json.Unmarshal([]byte(oldInviteUserIDsStr), &oldInviteUserIDs)\n\t\tif err == nil {\n\t\t\tneedSendNotificationUserIDs = converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs)\n\t\t}\n\t} else {\n\t\tneedSendNotificationUserIDs = inviteUserIDs\n\t}\n\tgo qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID)\n\n\treturn nil\n}\n\nfunc (qs *QuestionService) notificationInviteUser(\n\tctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) {\n\tinviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", questionUserID)\n\t\treturn\n\t}\n\n\tusers, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tinvitee := make(map[string]*entity.User, len(users))\n\tfor _, user := range users {\n\t\tinvitee[user.ID] = user\n\t}\n\tfor _, userID := range invitedUserIDs {\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tReceiverUserID: userID,\n\t\t\tTriggerUserID:  questionUserID,\n\t\t\tType:           schema.NotificationTypeInbox,\n\t\t\tObjectID:       questionID,\n\t\t}\n\t\tmsg.ObjectType = constant.QuestionObjectType\n\t\tmsg.NotificationAction = constant.NotificationInvitedYouToAnswer\n\t\tqs.notificationQueueService.Send(ctx, msg)\n\n\t\treceiverUserInfo, ok := invitee[userID]\n\t\tif !ok {\n\t\t\tlog.Warnf(\"user %s not found\", userID)\n\t\t\treturn\n\t\t}\n\t\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\t\tReceiverUserID: receiverUserInfo.ID,\n\t\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\t\tReceiverLang:   receiverUserInfo.Language,\n\t\t}\n\t\trawData := &schema.NewInviteAnswerTemplateRawData{\n\t\t\tInviterDisplayName: inviter.DisplayName,\n\t\t\tQuestionTitle:      questionTitle,\n\t\t\tQuestionID:         questionID,\n\t\t\tUnsubscribeCode:    token.GenerateToken(),\n\t\t}\n\t\texternalNotificationMsg.NewInviteAnswerTemplateRawData = rawData\n\t\tqs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n\t}\n}\n\n// UpdateQuestion update question\nfunc (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {\n\tvar canUpdate bool\n\tquestionInfo = &schema.QuestionInfoResp{}\n\n\t_, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif existUnreviewed {\n\t\terr = errors.BadRequest(reason.QuestionCannotUpdate)\n\t\treturn\n\t}\n\n\tdbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !has {\n\t\treturn\n\t}\n\tif dbinfo.Status == entity.QuestionStatusDeleted {\n\t\terr = errors.BadRequest(reason.QuestionCannotUpdate)\n\t\treturn nil, err\n\t}\n\n\tnow := time.Now()\n\tquestion := &entity.Question{}\n\tquestion.Title = req.Title\n\tquestion.OriginalText = req.Content\n\tquestion.ParsedText = req.HTML\n\tquestion.ID = uid.DeShortID(req.ID)\n\tquestion.UpdatedAt = now\n\tquestion.PostUpdateTime = now\n\tquestion.UserID = dbinfo.UserID\n\tquestion.LastEditUserID = req.UserID\n\n\tminimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(req.Content) < minimumContentLength {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"content\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),\n\t\t})\n\t\terr = errors.BadRequest(reason.QuestionContentLessThanMinimum)\n\t\treturn errorlist, err\n\t}\n\n\toldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)\n\tif tagerr != nil {\n\t\treturn questionInfo, tagerr\n\t}\n\n\ttagNameList := make([]string, 0)\n\toldtagNameList := make([]string, 0)\n\tfor _, tag := range req.Tags {\n\t\ttag.SlugName = strings.ReplaceAll(tag.SlugName, \" \", \"-\")\n\t\ttagNameList = append(tagNameList, tag.SlugName)\n\t}\n\tfor _, tag := range oldTags {\n\t\toldtagNameList = append(oldtagNameList, tag.SlugName)\n\t}\n\n\tisChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)\n\n\t// If the content is the same, ignore it\n\tif dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {\n\t\treturn\n\t}\n\n\tTags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)\n\tif tagerr != nil {\n\t\treturn questionInfo, tagerr\n\t}\n\n\t// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.\n\tif !req.CanUseReservedTag {\n\t\tCheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)\n\t\tif !CheckOldTag {\n\t\t\terrMsg := fmt.Sprintf(`The reserved tag \"%s\" must be present.`,\n\t\t\t\tstrings.Join(CheckOldTaglist, \",\"))\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t\t\treturn errorlist, err\n\t\t}\n\t\tif !CheckNewTag {\n\t\t\terrMsg := fmt.Sprintf(`\"%s\" can only be used by moderators.`,\n\t\t\t\tstrings.Join(CheckNewTaglist, \",\"))\n\t\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\t\tErrorField: \"tags\",\n\t\t\t\tErrorMsg:   errMsg,\n\t\t\t})\n\t\t\terr = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)\n\t\t\treturn errorlist, err\n\t\t}\n\t}\n\t// Check whether mandatory labels are selected\n\trecommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !recommendExist {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),\n\t\t})\n\t\terr = errors.BadRequest(reason.RecommendTagEnter)\n\t\treturn errorlist, err\n\t}\n\n\t// Administrators and themselves do not need to be audited\n\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   question.UserID,\n\t\tObjectID: question.ID,\n\t\tTitle:    question.Title,\n\t\tLog:      req.EditSummary,\n\t}\n\n\tif req.NoNeedReview {\n\t\tcanUpdate = true\n\t}\n\n\t// It's not you or the administrator that needs to be reviewed\n\tif !canUpdate {\n\t\trevisionDTO.Status = entity.RevisionUnreviewedStatus\n\t\trevisionDTO.UserID = req.UserID // use revision userid\n\t} else {\n\t\t// Direct modification\n\t\trevisionDTO.Status = entity.RevisionReviewPassStatus\n\t\t// update question to db\n\t\tquestion.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, \"\", question.ParsedText, question.OriginalText)\n\t\tif err != nil {\n\t\t\treturn questionInfo, err\n\t\t}\n\t\tsaveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{\"title\", \"original_text\", \"parsed_text\", \"updated_at\", \"post_update_time\", \"last_edit_user_id\"})\n\t\tif saveerr != nil {\n\t\t\treturn questionInfo, saveerr\n\t\t}\n\t\tobjectTagData := schema.TagChange{}\n\t\tobjectTagData.ObjectID = question.ID\n\t\tobjectTagData.Tags = req.Tags\n\t\tobjectTagData.UserID = req.UserID\n\t\terrorlist, tagerr := qs.ChangeTag(ctx, &objectTagData)\n\t\tif tagerr != nil {\n\t\t\treturn errorlist, tagerr\n\t\t}\n\t}\n\n\tquestionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, Tags)\n\tinfoJSON, _ := json.Marshal(questionWithTagsRevision)\n\trevisionDTO.Content = string(infoJSON)\n\trevisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn\n\t}\n\tif canUpdate {\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           req.UserID,\n\t\t\tObjectID:         question.ID,\n\t\t\tActivityTypeKey:  constant.ActQuestionEdited,\n\t\t\tRevisionID:       revisionID,\n\t\t\tOriginalObjectID: question.ID,\n\t\t})\n\t\tqs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID).TID(question.ID).\n\t\t\tQID(question.ID, question.UserID))\n\t}\n\n\tquestionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)\n\treturn\n}\n\n// GetQuestion get question one\nfunc (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string,\n\tper schema.QuestionPermission) (resp *schema.QuestionInfoResp, err error) {\n\tquestion, err := qs.questioncommon.Info(ctx, questionID, userID)\n\tif err != nil {\n\t\treturn\n\t}\n\t// If the question is deleted or pending, only the administrator and the author can view it\n\tif (question.Status == entity.QuestionStatusDeleted ||\n\t\tquestion.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID {\n\t\treturn nil, errors.NotFound(reason.QuestionNotFound)\n\t}\n\tif question.Status != entity.QuestionStatusClosed {\n\t\tper.CanReopen = false\n\t}\n\tif question.Status == entity.QuestionStatusClosed {\n\t\tper.CanClose = false\n\t}\n\tif question.Pin == entity.QuestionPin {\n\t\tper.CanPin = false\n\t\tper.CanHide = false\n\t}\n\tif question.Pin == entity.QuestionUnPin {\n\t\tper.CanUnPin = false\n\t}\n\tif question.Show == entity.QuestionShow {\n\t\tper.CanShow = false\n\t}\n\tif question.Show == entity.QuestionHide {\n\t\tper.CanHide = false\n\t\tper.CanPin = false\n\t}\n\n\tif question.Status == entity.QuestionStatusDeleted {\n\t\toperation := &schema.Operation{}\n\t\toperation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)\n\t\toperation.Level = schema.OperationLevelDanger\n\t\tquestion.Operation = operation\n\t}\n\tif question.Status == entity.QuestionStatusPending {\n\t\toperation := &schema.Operation{}\n\t\toperation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionUnderReview)\n\t\toperation.Level = schema.OperationLevelSecondary\n\t\tquestion.Operation = operation\n\t}\n\n\tquestion.Description = htmltext.FetchExcerpt(question.HTML, \"...\", 240)\n\tquestion.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.Status,\n\t\tper.CanEdit, per.CanDelete,\n\t\tper.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow,\n\t\tper.CanRecover)\n\tquestion.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, per.CanInviteOtherToAnswer)\n\treturn question, nil\n}\n\n// GetQuestionAndAddPV get question one\nfunc (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string,\n\tper schema.QuestionPermission) (\n\tresp *schema.QuestionInfoResp, err error) {\n\terr = qs.questioncommon.UpdatePv(ctx, questionID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn qs.GetQuestion(ctx, questionID, loginUserID, per)\n}\n\nfunc (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {\n\treturn qs.questioncommon.InviteUserInfo(ctx, questionID)\n}\n\nfunc (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) (errorlist []*validator.FormErrorField, err error) {\n\tminimumTags, err := qs.tagCommon.GetMinimumTags(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn qs.tagCommon.ObjectChangeTag(ctx, objectTagData, minimumTags)\n}\n\nfunc (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {\n\treturn qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)\n}\n\n// PersonalQuestionPage get question list by user\nfunc (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tuserinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tsearch := &schema.QuestionPageReq{}\n\tsearch.OrderCond = req.OrderCond\n\tsearch.Page = req.Page\n\tsearch.PageSize = req.PageSize\n\tsearch.UserIDBeSearched = userinfo.ID\n\tsearch.LoginUserID = req.LoginUserID\n\t// Only author and administrator can view the pending question\n\tif req.LoginUserID == userinfo.ID || req.IsAdmin {\n\t\tsearch.ShowPending = true\n\t}\n\tquestionList, total, err := qs.GetQuestionPage(ctx, search)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserQuestionInfoList := make([]*schema.UserQuestionInfo, 0)\n\tfor _, item := range questionList {\n\t\tinfo := &schema.UserQuestionInfo{}\n\t\t_ = copier.Copy(info, item)\n\t\tstatus, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]\n\t\tif ok {\n\t\t\tinfo.Status = status\n\t\t}\n\t\tuserQuestionInfoList = append(userQuestionInfoList, info)\n\t}\n\treturn pager.NewPageModel(total, userQuestionInfoList), nil\n}\n\nfunc (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tuserinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tcond := &entity.PersonalAnswerPageQueryCond{}\n\tcond.UserID = userinfo.ID\n\tcond.Page = req.Page\n\tcond.PageSize = req.PageSize\n\tcond.ShowPending = req.IsAdmin || req.LoginUserID == cond.UserID\n\tif req.OrderCond == \"newest\" {\n\t\tcond.Order = entity.AnswerSearchOrderByTime\n\t} else {\n\t\tcond.Order = entity.AnswerSearchOrderByDefault\n\t}\n\tquestionIDs := make([]string, 0)\n\tanswerList, total, err := qs.questioncommon.AnswerCommon.PersonalAnswerPage(ctx, cond)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswerlist := make([]*schema.AnswerInfo, 0)\n\tuserAnswerlist := make([]*schema.UserAnswerInfo, 0)\n\tfor _, item := range answerList {\n\t\tanswerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)\n\t\tanswerlist = append(answerlist, answerinfo)\n\t\tquestionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))\n\t}\n\tquestionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, item := range answerlist {\n\t\t_, ok := questionMaps[item.QuestionID]\n\t\tif ok {\n\t\t\titem.QuestionInfo = questionMaps[item.QuestionID]\n\t\t} else {\n\t\t\tcontinue\n\t\t}\n\t\tinfo := &schema.UserAnswerInfo{}\n\t\t_ = copier.Copy(info, item)\n\t\tinfo.AnswerID = item.ID\n\t\tinfo.QuestionID = item.QuestionID\n\t\tif item.QuestionInfo.Status == entity.QuestionStatusDeleted {\n\t\t\tinfo.QuestionInfo.Title = \"Deleted question\"\n\t\t}\n\t\tuserAnswerlist = append(userAnswerlist, info)\n\t}\n\n\treturn pager.NewPageModel(total, userAnswerlist), nil\n}\n\n// PersonalCollectionPage get collection list by user\nfunc (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tlist := make([]*schema.QuestionInfoResp, 0)\n\tcollectionSearch := &entity.CollectionSearch{}\n\tcollectionSearch.UserID = req.UserID\n\tcollectionSearch.Page = req.Page\n\tcollectionSearch.PageSize = req.PageSize\n\tcollectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tquestionIDs := make([]string, 0)\n\tfor _, item := range collectionList {\n\t\tquestionIDs = append(questionIDs, item.ObjectID)\n\t}\n\n\tquestionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, id := range questionIDs {\n\t\tif handler.GetEnableShortID(ctx) {\n\t\t\tid = uid.EnShortID(id)\n\t\t}\n\t\t_, ok := questionMaps[id]\n\t\tif ok {\n\t\t\tquestionMaps[id].LastAnsweredUserInfo = nil\n\t\t\tquestionMaps[id].UpdateUserInfo = nil\n\t\t\tquestionMaps[id].Content = \"\"\n\t\t\tquestionMaps[id].HTML = \"\"\n\t\t\tif questionMaps[id].Status == entity.QuestionStatusDeleted {\n\t\t\t\tquestionMaps[id].Title = \"Deleted question\"\n\t\t\t}\n\t\t\tlist = append(list, questionMaps[id])\n\t\t}\n\t}\n\n\treturn pager.NewPageModel(total, list), nil\n}\n\nfunc (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {\n\tanswerlist := make([]*schema.AnswerInfo, 0)\n\n\tuserAnswerlist := make([]*schema.UserAnswerInfo, 0)\n\tuserQuestionlist := make([]*schema.UserQuestionInfo, 0)\n\n\tuserinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)\n\tif err != nil {\n\t\treturn userQuestionlist, userAnswerlist, err\n\t}\n\tif !Exist {\n\t\treturn userQuestionlist, userAnswerlist, nil\n\t}\n\tsearch := &schema.QuestionPageReq{}\n\tsearch.OrderCond = \"score\"\n\tsearch.Page = 0\n\tsearch.PageSize = 5\n\tsearch.UserIDBeSearched = userinfo.ID\n\tsearch.LoginUserID = loginUserID\n\tquestionlist, _, err := qs.GetQuestionPage(ctx, search)\n\tif err != nil {\n\t\treturn userQuestionlist, userAnswerlist, err\n\t}\n\tanswersearch := &entity.AnswerSearch{}\n\tanswersearch.UserID = userinfo.ID\n\tanswersearch.PageSize = 5\n\tanswersearch.Order = entity.AnswerSearchOrderByVote\n\tquestionIDs := make([]string, 0)\n\tanswerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)\n\tif err != nil {\n\t\treturn userQuestionlist, userAnswerlist, err\n\t}\n\tfor _, item := range answerList {\n\t\tanswerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)\n\t\tanswerlist = append(answerlist, answerinfo)\n\t\tquestionIDs = append(questionIDs, item.QuestionID)\n\t}\n\tquestionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)\n\tif err != nil {\n\t\treturn userQuestionlist, userAnswerlist, err\n\t}\n\tfor _, item := range answerlist {\n\t\t_, ok := questionMaps[item.QuestionID]\n\t\tif ok {\n\t\t\titem.QuestionInfo = questionMaps[item.QuestionID]\n\t\t}\n\t}\n\n\tfor _, item := range questionlist {\n\t\tinfo := &schema.UserQuestionInfo{}\n\t\t_ = copier.Copy(info, item)\n\t\tinfo.UrlTitle = htmltext.UrlTitle(info.Title)\n\t\tuserQuestionlist = append(userQuestionlist, info)\n\t}\n\n\tfor _, item := range answerlist {\n\t\tinfo := &schema.UserAnswerInfo{}\n\t\t_ = copier.Copy(info, item)\n\t\tinfo.AnswerID = item.ID\n\t\tinfo.QuestionID = item.QuestionID\n\t\tinfo.QuestionInfo.UrlTitle = htmltext.UrlTitle(info.QuestionInfo.Title)\n\t\tuserAnswerlist = append(userAnswerlist, info)\n\t}\n\n\treturn userQuestionlist, userAnswerlist, nil\n}\n\n// GetQuestionsByTitle get questions by title\nfunc (qs *QuestionService) GetQuestionsByTitle(ctx context.Context, title string) (\n\tresp []*schema.QuestionBaseInfo, err error) {\n\tresp = make([]*schema.QuestionBaseInfo, 0)\n\tif len(title) == 0 {\n\t\treturn resp, nil\n\t}\n\t// check search plugin\n\tvar finder plugin.Search\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\tfinder = search\n\t\treturn nil\n\t})\n\n\tvar questions []*entity.Question\n\tif finder != nil {\n\t\t// call search plugin if available\n\t\twords := []string{title}\n\t\tres, _, err := finder.SearchQuestions(ctx, &plugin.SearchBasicCond{\n\t\t\tWords:    words,\n\t\t\tPage:     1,\n\t\t\tPageSize: 10,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn resp, err\n\t\t}\n\t\t// get question ids from res\n\t\tquestionIDs := make([]string, 0)\n\t\tfor _, question := range res {\n\t\t\tquestionIDs = append(questionIDs, question.ID)\n\t\t}\n\t\tvar questionErr error\n\t\tquestions, questionErr = qs.questionRepo.FindByID(ctx, questionIDs)\n\t\tif questionErr != nil {\n\t\t\treturn resp, questionErr\n\t\t}\n\t} else {\n\t\tvar questionErr error\n\t\tquestions, questionErr = qs.questionRepo.GetQuestionsByTitle(ctx, title, 10)\n\t\tif questionErr != nil {\n\t\t\treturn resp, questionErr\n\t\t}\n\t}\n\tfor _, question := range questions {\n\t\titem := &schema.QuestionBaseInfo{}\n\t\titem.ID = question.ID\n\t\titem.Title = question.Title\n\t\titem.UrlTitle = htmltext.UrlTitle(question.Title)\n\t\titem.ViewCount = question.ViewCount\n\t\titem.AnswerCount = question.AnswerCount\n\t\titem.CollectionCount = question.CollectionCount\n\t\titem.FollowCount = question.FollowCount\n\t\tstatus, ok := entity.AdminQuestionSearchStatusIntToString[question.Status]\n\t\tif ok {\n\t\t\titem.Status = status\n\t\t}\n\t\tif question.AcceptedAnswerID != \"0\" {\n\t\t\titem.AcceptedAnswer = true\n\t\t}\n\t\tresp = append(resp, item)\n\t}\n\treturn resp, nil\n}\n\n// SimilarQuestion\nfunc (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) {\n\tquestion, err := qs.questioncommon.Info(ctx, questionID, loginUserID)\n\tif err != nil {\n\t\treturn nil, 0, nil\n\t}\n\ttagNames := make([]string, 0, len(question.Tags))\n\tfor _, tag := range question.Tags {\n\t\ttagNames = append(tagNames, tag.SlugName)\n\t}\n\tsearch := &schema.QuestionPageReq{}\n\tsearch.OrderCond = \"hot\"\n\tsearch.Page = 0\n\tsearch.PageSize = 6\n\tif len(tagNames) > 0 {\n\t\tsearch.Tag = tagNames[0]\n\t}\n\tsearch.LoginUserID = loginUserID\n\tsimilarQuestions, _, err := qs.GetQuestionPage(ctx, search)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tvar result []*schema.QuestionPageResp\n\tfor _, v := range similarQuestions {\n\t\tif uid.DeShortID(v.ID) != questionID {\n\t\t\tresult = append(result, v)\n\t\t}\n\t}\n\treturn result, int64(len(result)), nil\n}\n\n// GetQuestionPage query questions page\nfunc (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (\n\tquestions []*schema.QuestionPageResp, total int64, err error) {\n\tquestions = make([]*schema.QuestionPageResp, 0)\n\t// query by user role\n\tshowHidden := false\n\tif req.LoginUserID != \"\" && req.UserIDBeSearched != \"\" {\n\t\tshowHidden = req.LoginUserID == req.UserIDBeSearched\n\t\tif !showHidden {\n\t\t\tuserRole, err := qs.userRoleRelService.GetUserRole(ctx, req.LoginUserID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\t\t\tshowHidden = userRole == role.RoleAdminID || userRole == role.RoleModeratorID\n\t\t}\n\t}\n\t// query by tag condition\n\tvar tagIDs = make([]string, 0)\n\tif len(req.Tag) > 0 {\n\t\ttagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tif exist {\n\t\t\tsynTagIds, err := qs.tagCommon.GetTagIDsByMainTagID(ctx, tagInfo.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\t\t\ttagIDs = append(tagIDs, synTagIds...)\n\t\t\ttagIDs = append(tagIDs, tagInfo.ID)\n\t\t} else {\n\t\t\treturn questions, 0, nil\n\t\t}\n\t}\n\n\t// query by user condition\n\tif req.Username != \"\" {\n\t\tuserinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn questions, 0, nil\n\t\t}\n\t\treq.UserIDBeSearched = userinfo.ID\n\t}\n\n\tif req.OrderCond == schema.QuestionOrderCondHot {\n\t\treq.InDays = schema.HotInDays\n\t}\n\n\tquestionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,\n\t\ttagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden, req.ShowPending)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tquestions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn questions, total, nil\n}\n\n// GetRecommendQuestionPage retrieves recommended question page based on following tags and questions.\nfunc (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (\n\tquestions []*schema.QuestionPageResp, total int64, err error) {\n\tfollowingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\ttagIDs := make([]string, 0, len(followingTagsResp))\n\tfor _, tag := range followingTagsResp {\n\t\ttagIDs = append(tagIDs, tag.TagID)\n\t}\n\n\tactivityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, \"follow\")\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tactivities, err := qs.activityRepo.GetUserActivitiesByActivityType(ctx, req.LoginUserID, activityType)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tfollowedQuestionIDs := make([]string, 0, len(activities))\n\tfor _, activity := range activities {\n\t\tif activity.Cancelled == entity.ActivityCancelled {\n\t\t\tcontinue\n\t\t}\n\t\tfollowedQuestionIDs = append(followedQuestionIDs, activity.ObjectID)\n\t}\n\tquestionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tquestions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, schema.QuestionOrderCondFrequent)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn questions, total, nil\n}\n\nfunc (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error {\n\tsetStatus, ok := entity.AdminQuestionSearchStatus[req.Status]\n\tif !ok {\n\t\treturn errors.BadRequest(reason.RequestFormatError)\n\t}\n\tquestionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.QuestionNotFound)\n\t}\n\terr = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, setStatus)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmsg := &schema.NotificationMsg{}\n\tif setStatus == entity.QuestionStatusDeleted {\n\t\t// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,\n\t\t// facing the problem of recovery.\n\t\t// err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)\n\t\t// if err != nil {\n\t\t// \tlog.Errorf(\"admin delete question then rank rollback error %s\", err.Error())\n\t\t// }\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           questionInfo.UserID,\n\t\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\t\tObjectID:         questionInfo.ID,\n\t\t\tOriginalObjectID: questionInfo.ID,\n\t\t\tActivityTypeKey:  constant.ActQuestionDeleted,\n\t\t})\n\t\tmsg.NotificationAction = constant.NotificationYourQuestionWasDeleted\n\t}\n\tif setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           questionInfo.UserID,\n\t\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\t\tObjectID:         questionInfo.ID,\n\t\t\tOriginalObjectID: questionInfo.ID,\n\t\t\tActivityTypeKey:  constant.ActQuestionReopened,\n\t\t})\n\t}\n\tif setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           questionInfo.UserID,\n\t\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\t\tObjectID:         questionInfo.ID,\n\t\t\tOriginalObjectID: questionInfo.ID,\n\t\t\tActivityTypeKey:  constant.ActQuestionClosed,\n\t\t})\n\t\tmsg.NotificationAction = constant.NotificationYourQuestionIsClosed\n\t}\n\t// recover\n\tif setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusDeleted {\n\t\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           req.UserID,\n\t\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\t\tObjectID:         questionInfo.ID,\n\t\t\tOriginalObjectID: questionInfo.ID,\n\t\t\tActivityTypeKey:  constant.ActQuestionUndeleted,\n\t\t})\n\t}\n\n\tif len(msg.NotificationAction) > 0 {\n\t\tmsg.ObjectID = questionInfo.ID\n\t\tmsg.Type = schema.NotificationTypeInbox\n\t\tmsg.ReceiverUserID = questionInfo.UserID\n\t\tmsg.TriggerUserID = req.UserID\n\t\tmsg.ObjectType = constant.QuestionObjectType\n\t\tqs.notificationQueueService.Send(ctx, msg)\n\t}\n\treturn nil\n}\n\nfunc (qs *QuestionService) AdminQuestionPage(\n\tctx context.Context, req *schema.AdminQuestionPageReq) (\n\tresp *pager.PageModel, err error) {\n\tlist := make([]*schema.AdminQuestionInfo, 0)\n\tquestionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserIds := make([]string, 0)\n\tfor _, info := range questionList {\n\t\titem := &schema.AdminQuestionInfo{}\n\t\t_ = copier.Copy(item, info)\n\t\titem.CreateTime = info.CreatedAt.Unix()\n\t\titem.UpdateTime = info.PostUpdateTime.Unix()\n\t\titem.EditTime = info.UpdatedAt.Unix()\n\t\tlist = append(list, item)\n\t\tuserIds = append(userIds, info.UserID)\n\t}\n\tuserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, item := range list {\n\t\tif u, ok := userInfoMap[item.UserID]; ok {\n\t\t\titem.UserInfo = u\n\t\t}\n\t}\n\treturn pager.NewPageModel(count, list), nil\n}\n\n// AdminAnswerPage search answer list\nfunc (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) (\n\tresp *pager.PageModel, err error) {\n\tanswerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquestionIDs := make([]string, 0)\n\tuserIds := make([]string, 0)\n\tanswerResp := make([]*schema.AdminAnswerInfo, 0)\n\tfor _, item := range answerList {\n\t\tanswerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item)\n\t\tanswerResp = append(answerResp, answerInfo)\n\t\tquestionIDs = append(questionIDs, item.QuestionID)\n\t\tuserIds = append(userIds, item.UserID)\n\t}\n\tuserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tquestionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, item := range answerResp {\n\t\tif q, ok := questionMaps[item.QuestionID]; ok {\n\t\t\titem.QuestionInfo.Title = q.Title\n\t\t}\n\t\tif u, ok := userInfoMap[item.UserID]; ok {\n\t\t\titem.UserInfo = u\n\t\t}\n\t}\n\treturn pager.NewPageModel(count, answerResp), nil\n}\n\nfunc (qs *QuestionService) changeQuestionToRevision(_ context.Context, questionInfo *entity.Question, tags []*entity.Tag) (\n\tquestionRevision *entity.QuestionWithTagsRevision) {\n\tquestionRevision = &entity.QuestionWithTagsRevision{}\n\tquestionRevision.Question = *questionInfo\n\n\tfor _, tag := range tags {\n\t\titem := &entity.TagSimpleInfoForRevision{}\n\t\t_ = copier.Copy(item, tag)\n\t\tquestionRevision.Tags = append(questionRevision.Tags, item)\n\t}\n\treturn questionRevision\n}\n\nfunc (qs *QuestionService) SitemapCron(ctx context.Context) {\n\tsiteSeo, err := qs.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tctx = context.WithValue(ctx, constant.ShortIDContextKey, siteSeo.IsShortLink())\n\tqs.questioncommon.SitemapCron(ctx)\n}\n\nfunc (qs *QuestionService) GetQuestionLink(ctx context.Context, req *schema.GetQuestionLinkReq) (\n\tquestions []*schema.QuestionPageResp, total int64, err error) {\n\tif req.OrderCond == schema.QuestionOrderCondHot {\n\t\treq.InDays = schema.HotInDays\n\t}\n\n\tquestionList, total, err := qs.questionRepo.GetQuestionLink(ctx, req.Page, req.PageSize, req.QuestionID, req.OrderCond, req.InDays)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tquestions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn questions, total, nil\n}\n"
  },
  {
    "path": "internal/service/content/revision_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/report_common\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\t\"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// RevisionService user service\ntype RevisionService struct {\n\trevisionRepo             revision.RevisionRepo\n\tuserCommon               *usercommon.UserCommon\n\tquestionCommon           *questioncommon.QuestionCommon\n\tanswerService            *AnswerService\n\tobjectInfoService        *object_info.ObjService\n\tquestionRepo             questioncommon.QuestionRepo\n\tanswerRepo               answercommon.AnswerRepo\n\ttagRepo                  tag_common.TagRepo\n\ttagCommon                *tag_common.TagCommonService\n\tnotificationQueueService noticequeue.Service\n\tactivityQueueService     activityqueue.Service\n\treportRepo               report_common.ReportRepo\n\treviewService            *review.ReviewService\n\treviewActivity           activity.ReviewActivityRepo\n}\n\nfunc NewRevisionService(\n\trevisionRepo revision.RevisionRepo,\n\tuserCommon *usercommon.UserCommon,\n\tquestionCommon *questioncommon.QuestionCommon,\n\tanswerService *AnswerService,\n\tobjectInfoService *object_info.ObjService,\n\tquestionRepo questioncommon.QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\ttagRepo tag_common.TagRepo,\n\ttagCommon *tag_common.TagCommonService,\n\tnotificationQueueService noticequeue.Service,\n\tactivityQueueService activityqueue.Service,\n\treportRepo report_common.ReportRepo,\n\treviewService *review.ReviewService,\n\treviewActivity activity.ReviewActivityRepo,\n) *RevisionService {\n\treturn &RevisionService{\n\t\trevisionRepo:             revisionRepo,\n\t\tuserCommon:               userCommon,\n\t\tquestionCommon:           questionCommon,\n\t\tanswerService:            answerService,\n\t\tobjectInfoService:        objectInfoService,\n\t\tquestionRepo:             questionRepo,\n\t\tanswerRepo:               answerRepo,\n\t\ttagRepo:                  tagRepo,\n\t\ttagCommon:                tagCommon,\n\t\tnotificationQueueService: notificationQueueService,\n\t\tactivityQueueService:     activityQueueService,\n\t\treportRepo:               reportRepo,\n\t\treviewService:            reviewService,\n\t\treviewActivity:           reviewActivity,\n\t}\n}\n\nfunc (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) {\n\trevisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn\n\t}\n\tif revisioninfo.Status != entity.RevisionUnreviewedStatus {\n\t\treturn\n\t}\n\tif req.Operation == schema.RevisionAuditReject {\n\t\terr = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID)\n\t\treturn\n\t}\n\tif req.Operation == schema.RevisionAuditApprove {\n\t\tobjectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID)\n\t\tif objectTypeerr != nil {\n\t\t\treturn objectTypeerr\n\t\t}\n\t\trevisionitem := &schema.GetRevisionResp{}\n\t\t_ = copier.Copy(revisionitem, revisioninfo)\n\t\trs.parseItem(ctx, revisionitem)\n\t\tvar saveErr error\n\t\tswitch objectType {\n\t\tcase constant.QuestionObjectType:\n\t\t\tif !req.CanReviewQuestion {\n\t\t\t\tsaveErr = errors.BadRequest(reason.RevisionNoPermission)\n\t\t\t} else {\n\t\t\t\tsaveErr = rs.revisionAuditQuestion(ctx, revisionitem)\n\t\t\t}\n\t\tcase constant.AnswerObjectType:\n\t\t\tif !req.CanReviewAnswer {\n\t\t\t\tsaveErr = errors.BadRequest(reason.RevisionNoPermission)\n\t\t\t} else {\n\t\t\t\tsaveErr = rs.revisionAuditAnswer(ctx, revisionitem)\n\t\t\t}\n\t\tcase constant.TagObjectType:\n\t\t\tif !req.CanReviewTag {\n\t\t\t\tsaveErr = errors.BadRequest(reason.RevisionNoPermission)\n\t\t\t} else {\n\t\t\t\tsaveErr = rs.revisionAuditTag(ctx, revisionitem)\n\t\t\t}\n\t\t}\n\t\tif saveErr != nil {\n\t\t\treturn saveErr\n\t\t}\n\t\terr = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{\n\t\t\tUserID:           revisioninfo.UserID,\n\t\t\tTriggerUserID:    req.UserID,\n\t\t\tObjectID:         revisioninfo.ObjectID,\n\t\t\tOriginalObjectID: \"0\",\n\t\t\tRevisionID:       revisioninfo.ID,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"add review activity failed: %v\", err)\n\t\t}\n\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tTriggerUserID:  req.UserID,\n\t\t\tReceiverUserID: revisioninfo.UserID,\n\t\t\tType:           schema.NotificationTypeAchievement,\n\t\t\tObjectID:       revisioninfo.ObjectID,\n\t\t\tObjectType:     objectType,\n\t\t}\n\t\trs.notificationQueueService.Send(ctx, msg)\n\t\treturn\n\t}\n\n\treturn nil\n}\n\nfunc (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {\n\tquestioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp)\n\tif ok {\n\t\tvar PostUpdateTime time.Time\n\t\tdbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID)\n\t\tif dberr != nil || !exist {\n\t\t\treturn\n\t\t}\n\n\t\tPostUpdateTime = time.Unix(questioninfo.UpdateTime, 0)\n\t\tif dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {\n\t\t\tPostUpdateTime = dbquestion.PostUpdateTime\n\t\t}\n\t\tquestion := &entity.Question{}\n\t\tquestion.ID = questioninfo.ID\n\t\tquestion.Title = questioninfo.Title\n\t\tquestion.OriginalText = questioninfo.Content\n\t\tquestion.ParsedText = questioninfo.HTML\n\t\tquestion.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0)\n\t\tquestion.PostUpdateTime = PostUpdateTime\n\t\tquestion.LastEditUserID = revisionitem.UserID\n\t\tsaveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{\"title\", \"original_text\", \"parsed_text\", \"updated_at\", \"post_update_time\", \"last_edit_user_id\"})\n\t\tif saveerr != nil {\n\t\t\treturn saveerr\n\t\t}\n\t\tobjectTagTags := make([]*schema.TagItem, 0)\n\t\tfor _, tag := range questioninfo.Tags {\n\t\t\titem := &schema.TagItem{}\n\t\t\titem.SlugName = tag.SlugName\n\t\t\tobjectTagTags = append(objectTagTags, item)\n\t\t}\n\t\tobjectTagData := schema.TagChange{}\n\t\tobjectTagData.ObjectID = question.ID\n\t\tobjectTagData.Tags = objectTagTags\n\t\tminimumTags, err := rs.tagCommon.GetMinimumTags(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags)\n\t\tif saveerr != nil {\n\t\t\treturn saveerr\n\t\t}\n\t\trs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           revisionitem.UserID,\n\t\t\tObjectID:         revisionitem.ObjectID,\n\t\t\tActivityTypeKey:  constant.ActQuestionEdited,\n\t\t\tRevisionID:       revisionitem.ID,\n\t\t\tOriginalObjectID: revisionitem.ObjectID,\n\t\t})\n\t}\n\treturn nil\n}\n\nfunc (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {\n\tanswerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo)\n\tif ok {\n\t\tvar PostUpdateTime time.Time\n\t\tdbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)\n\t\tif dberr != nil || !exist {\n\t\t\treturn\n\t\t}\n\n\t\tPostUpdateTime = time.Unix(answerinfo.UpdateTime, 0)\n\t\tif dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {\n\t\t\tPostUpdateTime = dbquestion.PostUpdateTime\n\t\t}\n\n\t\tinsertData := new(entity.Answer)\n\t\tinsertData.ID = answerinfo.ID\n\t\tinsertData.OriginalText = answerinfo.Content\n\t\tinsertData.ParsedText = answerinfo.HTML\n\t\tinsertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0)\n\t\tinsertData.LastEditUserID = revisionitem.UserID\n\t\tsaveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{\"original_text\", \"parsed_text\", \"updated_at\", \"last_edit_user_id\"})\n\t\tif saveerr != nil {\n\t\t\treturn saveerr\n\t\t}\n\t\tsaveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)\n\t\tif saveerr != nil {\n\t\t\treturn saveerr\n\t\t}\n\t\tquestionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.QuestionNotFound)\n\t\t}\n\t\tmsg := &schema.NotificationMsg{\n\t\t\tTriggerUserID:  revisionitem.UserID,\n\t\t\tReceiverUserID: questionInfo.UserID,\n\t\t\tType:           schema.NotificationTypeInbox,\n\t\t\tObjectID:       answerinfo.ID,\n\t\t}\n\t\tmsg.ObjectType = constant.AnswerObjectType\n\t\tmsg.NotificationAction = constant.NotificationUpdateAnswer\n\t\trs.notificationQueueService.Send(ctx, msg)\n\n\t\trs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           revisionitem.UserID,\n\t\t\tObjectID:         insertData.ID,\n\t\t\tOriginalObjectID: insertData.ID,\n\t\t\tActivityTypeKey:  constant.ActAnswerEdited,\n\t\t\tRevisionID:       revisionitem.ID,\n\t\t})\n\t}\n\treturn nil\n}\n\nfunc (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {\n\ttaginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp)\n\tif ok {\n\t\ttag := &entity.Tag{}\n\t\ttag.ID = taginfo.TagID\n\t\ttag.OriginalText = taginfo.OriginalText\n\t\ttag.ParsedText = taginfo.ParsedText\n\t\tsaveerr := rs.tagRepo.UpdateTag(ctx, tag)\n\t\tif saveerr != nil {\n\t\t\treturn saveerr\n\t\t}\n\n\t\ttagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.TagNotFound)\n\t\t}\n\t\tif tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 {\n\t\t\tlog.Debugf(\"tag %s update slug_name\", tagInfo.SlugName)\n\t\t\ttagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tupdateTagSlugNames := make([]string, 0)\n\t\t\tfor _, tag := range tagList {\n\t\t\t\tupdateTagSlugNames = append(updateTagSlugNames, tag.SlugName)\n\t\t\t}\n\t\t\terr = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\trs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           revisionitem.UserID,\n\t\t\tObjectID:         taginfo.TagID,\n\t\t\tOriginalObjectID: taginfo.TagID,\n\t\t\tActivityTypeKey:  constant.ActTagEdited,\n\t\t\tRevisionID:       revisionitem.ID,\n\t\t})\n\t}\n\treturn nil\n}\n\n// GetUnreviewedRevisionPage get unreviewed list\nfunc (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) (\n\tresp *pager.PageModel, err error) {\n\trevisionResp := make([]*schema.GetUnreviewedRevisionResp, 0)\n\tif len(req.GetCanReviewObjectTypes()) == 0 {\n\t\treturn pager.NewPageModel(0, revisionResp), nil\n\t}\n\trevisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage(\n\t\tctx, req.Page, 1, req.GetCanReviewObjectTypes())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, rev := range revisionPage {\n\t\titem := &schema.GetUnreviewedRevisionResp{}\n\t\t_, ok := constant.ObjectTypeNumberMapping[rev.ObjectType]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\titem.Type = constant.ObjectTypeNumberMapping[rev.ObjectType]\n\t\tinfo, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titem.Info = info\n\t\trevisionitem := &schema.GetRevisionResp{}\n\t\t_ = copier.Copy(revisionitem, rev)\n\t\trs.parseItem(ctx, revisionitem)\n\t\titem.UnreviewedInfo = revisionitem\n\n\t\t// get user info\n\t\tuserInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID)\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\tif exists {\n\t\t\tvar uinfo schema.UserBasicInfo\n\t\t\t_ = copier.Copy(&uinfo, userInfo)\n\t\t\titem.UnreviewedInfo.UserInfo = uinfo\n\t\t}\n\t\titem.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title)\n\t\titem.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title)\n\t\trevisionResp = append(revisionResp, item)\n\t}\n\treturn pager.NewPageModel(total, revisionResp), nil\n}\n\n// GetRevisionList get revision list all\nfunc (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) {\n\tvar (\n\t\trev  entity.Revision\n\t\trevs []entity.Revision\n\t)\n\n\tresp = []schema.GetRevisionResp{}\n\tobjInfo, infoErr := rs.objectInfoService.GetInfo(ctx, req.ObjectID)\n\tif infoErr != nil {\n\t\treturn nil, infoErr\n\t}\n\tif !req.IsAdmin && objInfo.IsDeleted() && objInfo.ObjectCreatorUserID != req.UserID {\n\t\tswitch objInfo.ObjectType {\n\t\tcase constant.QuestionObjectType:\n\t\t\treturn nil, errors.NotFound(reason.QuestionNotFound)\n\t\tcase constant.AnswerObjectType:\n\t\t\treturn nil, errors.NotFound(reason.AnswerNotFound)\n\t\tcase constant.TagObjectType:\n\t\t\treturn nil, errors.NotFound(reason.TagNotFound)\n\t\tdefault:\n\t\t\treturn nil, errors.NotFound(reason.ObjectNotFound)\n\t\t}\n\t}\n\n\t_ = copier.Copy(&rev, req)\n\n\trevs, err = rs.revisionRepo.GetRevisionList(ctx, &rev)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, r := range revs {\n\t\tvar (\n\t\t\tuinfo schema.UserBasicInfo\n\t\t\titem  schema.GetRevisionResp\n\t\t)\n\n\t\t_ = copier.Copy(&item, r)\n\t\trs.parseItem(ctx, &item)\n\n\t\t// get user info\n\t\tuserInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID)\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\tif exists {\n\t\t\terr = copier.Copy(&uinfo, userInfo)\n\t\t\titem.UserInfo = uinfo\n\t\t}\n\t\tresp = append(resp, item)\n\t}\n\treturn\n}\n\nfunc (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) {\n\tvar (\n\t\terr          error\n\t\tquestion     entity.QuestionWithTagsRevision\n\t\tquestionInfo *schema.QuestionInfoResp\n\t\tanswer       entity.Answer\n\t\tanswerInfo   *schema.AnswerInfo\n\t\ttag          entity.Tag\n\t\ttagInfo      *schema.GetTagResp\n\t)\n\n\tshortID := handler.GetEnableShortID(ctx)\n\tif shortID {\n\t\titem.ObjectID = uid.EnShortID(item.ObjectID)\n\t}\n\tswitch item.ObjectType {\n\tcase constant.ObjectTypeStrMapping[\"question\"]:\n\t\terr = json.Unmarshal([]byte(item.Content), &question)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tquestionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question)\n\t\tif shortID {\n\t\t\tquestionInfo.ID = uid.EnShortID(questionInfo.ID)\n\t\t}\n\t\titem.ContentParsed = questionInfo\n\tcase constant.ObjectTypeStrMapping[\"answer\"]:\n\t\terr = json.Unmarshal([]byte(item.Content), &answer)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tanswerInfo = rs.answerService.ShowFormat(ctx, &answer)\n\t\tif shortID {\n\t\t\tanswerInfo.ID = uid.EnShortID(answerInfo.ID)\n\t\t\tanswerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID)\n\t\t}\n\t\titem.ContentParsed = answerInfo\n\tcase constant.ObjectTypeStrMapping[\"tag\"]:\n\t\terr = json.Unmarshal([]byte(item.Content), &tag)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\ttagInfo = &schema.GetTagResp{\n\t\t\tTagID:         tag.ID,\n\t\t\tCreatedAt:     tag.CreatedAt.Unix(),\n\t\t\tUpdatedAt:     tag.UpdatedAt.Unix(),\n\t\t\tSlugName:      tag.SlugName,\n\t\t\tDisplayName:   tag.DisplayName,\n\t\t\tOriginalText:  tag.OriginalText,\n\t\t\tParsedText:    tag.ParsedText,\n\t\t\tFollowCount:   tag.FollowCount,\n\t\t\tQuestionCount: tag.QuestionCount,\n\t\t\tRecommend:     tag.Recommend,\n\t\t\tReserved:      tag.Reserved,\n\t\t}\n\t\ttagInfo.GetExcerpt()\n\t\titem.ContentParsed = tagInfo\n\t}\n\n\tif err != nil {\n\t\titem.ContentParsed = item.Content\n\t}\n\titem.CreatedAtParsed = item.CreatedAt.Unix()\n}\n\n// CheckCanUpdateRevision can check revision\nfunc (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) (\n\tresp *schema.ErrTypeData, err error) {\n\t_, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID)\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\tif exist {\n\t\treturn &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway)\n\t}\n\treturn nil, nil\n}\n\n// GetReviewingType get reviewing type\nfunc (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) {\n\tresp = make([]*schema.GetReviewingTypeResp, 0)\n\n\t// get queue amount\n\tif req.IsAdmin {\n\t\treviewCount, err := rs.reviewService.GetReviewPendingCount(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get report count failed: %v\", err)\n\t\t} else {\n\t\t\tresp = append(resp, &schema.GetReviewingTypeResp{\n\t\t\t\tName:       string(constant.QueuedPost),\n\t\t\t\tLabel:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel),\n\t\t\t\tTodoAmount: reviewCount,\n\t\t\t})\n\t\t}\n\t}\n\n\t// get flag amount\n\tif req.IsAdmin {\n\t\treportCount, err := rs.reportRepo.GetReportCount(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get report count failed: %v\", err)\n\t\t} else {\n\t\t\tresp = append(resp, &schema.GetReviewingTypeResp{\n\t\t\t\tName:       string(constant.FlaggedPost),\n\t\t\t\tLabel:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel),\n\t\t\t\tTodoAmount: reportCount,\n\t\t\t})\n\t\t}\n\t}\n\n\t// get suggestion amount\n\tcountUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes())\n\tif err != nil {\n\t\tlog.Errorf(\"get unreviewed revision count failed: %v\", err)\n\t} else {\n\t\tresp = append(resp, &schema.GetReviewingTypeResp{\n\t\t\tName:       string(constant.SuggestedPostEdit),\n\t\t\tLabel:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel),\n\t\t\tTodoAmount: countUnreviewedRevision,\n\t\t})\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/content/search_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/search_common\"\n\t\"github.com/apache/answer/internal/service/search_parser\"\n\t\"github.com/apache/answer/plugin\"\n)\n\ntype SearchService struct {\n\tsearchParser *search_parser.SearchParser\n\tsearchRepo   search_common.SearchRepo\n}\n\nfunc NewSearchService(\n\tsearchParser *search_parser.SearchParser,\n\tsearchRepo search_common.SearchRepo,\n) *SearchService {\n\treturn &SearchService{\n\t\tsearchParser: searchParser,\n\t\tsearchRepo:   searchRepo,\n\t}\n}\n\n// Search search contents\nfunc (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) {\n\tif dto.Page < 1 {\n\t\tdto.Page = 1\n\t}\n\tif len(dto.Query) == 0 {\n\t\treturn &schema.SearchResp{\n\t\t\tTotal:         0,\n\t\t\tSearchResults: make([]*schema.SearchResult, 0),\n\t\t}, nil\n\t}\n\n\t// search type\n\tcond := ss.searchParser.ParseStructure(ctx, dto)\n\n\t// check search plugin\n\tvar finder plugin.Search\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\tfinder = search\n\t\treturn nil\n\t})\n\n\tresp = &schema.SearchResp{}\n\t// search plugin is not found, call system search\n\tif finder == nil {\n\t\tswitch {\n\t\tcase cond.SearchAll():\n\t\t\tresp.SearchResults, resp.Total, err =\n\t\t\t\tss.searchRepo.SearchContents(ctx, cond.Words, cond.Tags, cond.UserID, cond.VoteAmount, dto.Page, dto.Size, dto.Order)\n\t\tcase cond.SearchQuestion():\n\t\t\tresp.SearchResults, resp.Total, err =\n\t\t\t\tss.searchRepo.SearchQuestions(ctx, cond.Words, cond.Tags, cond.NotAccepted, cond.Views, cond.AnswerAmount, dto.Page, dto.Size, dto.Order)\n\t\tcase cond.SearchAnswer():\n\t\t\tresp.SearchResults, resp.Total, err =\n\t\t\t\tss.searchRepo.SearchAnswers(ctx, cond.Words, cond.Tags, cond.Accepted, cond.QuestionID, dto.Page, dto.Size, dto.Order)\n\t\t}\n\t\treturn\n\t}\n\treturn ss.searchByPlugin(ctx, finder, cond, dto)\n}\n\nfunc (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Search, cond *schema.SearchCondition, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) {\n\tvar res []plugin.SearchResult\n\tresp = &schema.SearchResp{}\n\tswitch {\n\tcase cond.SearchAll():\n\t\tres, resp.Total, err = finder.SearchContents(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))\n\tcase cond.SearchQuestion():\n\t\tres, resp.Total, err = finder.SearchQuestions(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))\n\tcase cond.SearchAnswer():\n\t\tres, resp.Total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))\n\t}\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tresp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res, cond.Words)\n\treturn resp, err\n}\n"
  },
  {
    "path": "internal/service/content/user_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/apache/answer/pkg/token\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/file_record\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// UserService user service\ntype UserService struct {\n\tuserCommonService             *usercommon.UserCommon\n\tuserRepo                      usercommon.UserRepo\n\tuserActivity                  activity.UserActiveActivityRepo\n\tactivityRepo                  activity_common.ActivityRepo\n\temailService                  *export.EmailService\n\tauthService                   *auth.AuthService\n\tsiteInfoService               siteinfo_common.SiteInfoCommonService\n\tuserRoleService               *role.UserRoleRelService\n\tuserExternalLoginService      *user_external_login.UserExternalLoginService\n\tuserNotificationConfigRepo    user_notification_config.UserNotificationConfigRepo\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService\n\tquestionService               *questioncommon.QuestionCommon\n\teventQueueService             eventqueue.Service\n\tfileRecordService             *file_record.FileRecordService\n}\n\nfunc NewUserService(userRepo usercommon.UserRepo,\n\tuserActivity activity.UserActiveActivityRepo,\n\tactivityRepo activity_common.ActivityRepo,\n\temailService *export.EmailService,\n\tauthService *auth.AuthService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tuserRoleService *role.UserRoleRelService,\n\tuserCommonService *usercommon.UserCommon,\n\tuserExternalLoginService *user_external_login.UserExternalLoginService,\n\tuserNotificationConfigRepo user_notification_config.UserNotificationConfigRepo,\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService,\n\tquestionService *questioncommon.QuestionCommon,\n\teventQueueService eventqueue.Service,\n\tfileRecordService *file_record.FileRecordService,\n) *UserService {\n\treturn &UserService{\n\t\tuserCommonService:             userCommonService,\n\t\tuserRepo:                      userRepo,\n\t\tuserActivity:                  userActivity,\n\t\tactivityRepo:                  activityRepo,\n\t\temailService:                  emailService,\n\t\tauthService:                   authService,\n\t\tsiteInfoService:               siteInfoService,\n\t\tuserRoleService:               userRoleService,\n\t\tuserExternalLoginService:      userExternalLoginService,\n\t\tuserNotificationConfigRepo:    userNotificationConfigRepo,\n\t\tuserNotificationConfigService: userNotificationConfigService,\n\t\tquestionService:               questionService,\n\t\teventQueueService:             eventQueueService,\n\t\tfileRecordService:             fileRecordService,\n\t}\n}\n\n// GetUserInfoByUserID get user info by user id\nfunc (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (\n\tresp *schema.GetCurrentLoginUserInfoResp, err error) {\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tif userInfo.Status == entity.UserStatusDeleted {\n\t\treturn nil, errors.Unauthorized(reason.UnauthorizedError)\n\t}\n\n\tresp = &schema.GetCurrentLoginUserInfoResp{}\n\tresp.ConvertFromUserEntity(userInfo)\n\tresp.RoleID, err = us.userRoleService.GetUserRole(ctx, userInfo.ID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status)\n\tresp.AccessToken = token\n\tresp.HavePassword = len(userInfo.Pass) > 0\n\treturn resp, nil\n}\n\nfunc (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (\n\tresp *schema.GetOtherUserInfoByUsernameResp, err error) {\n\tuserInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.NotFound(reason.UserNotFound)\n\t}\n\tresp = &schema.GetOtherUserInfoByUsernameResp{}\n\tresp.ConvertFromUserEntityWithLang(ctx, userInfo)\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\n\t// Only the user himself and the administrator can see the hidden questions\n\tquestionCount, err := us.questionService.GetPersonalUserQuestionCount(ctx, req.UserID, userInfo.ID, req.IsAdmin)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.QuestionCount = int(questionCount)\n\treturn resp, nil\n}\n\n// EmailLogin email login\nfunc (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLoginReq) (resp *schema.UserLoginResp, err error) {\n\tsiteLogin, err := us.siteInfoService.GetSiteLogin(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !siteLogin.AllowPasswordLogin {\n\t\treturn nil, errors.BadRequest(reason.NotAllowedLoginViaPassword)\n\t}\n\tuserInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist || userInfo.Status == entity.UserStatusDeleted {\n\t\treturn nil, errors.BadRequest(reason.EmailOrPasswordWrong)\n\t}\n\tif !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {\n\t\treturn nil, errors.BadRequest(reason.EmailOrPasswordWrong)\n\t}\n\tok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !ok {\n\t\treturn nil, errors.BadRequest(reason.EmailOrPasswordWrong)\n\t}\n\n\terr = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID)\n\tif err != nil {\n\t\tlog.Errorf(\"update last login data failed, err: %v\", err)\n\t}\n\n\troleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp = &schema.UserLoginResp{}\n\tresp.ConvertFromUserEntity(userInfo)\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\tuserCacheInfo := &entity.UserCacheInfo{\n\t\tUserID:      userInfo.ID,\n\t\tEmailStatus: userInfo.MailStatus,\n\t\tUserStatus:  userInfo.Status,\n\t\tRoleID:      roleID,\n\t\tExternalID:  externalID,\n\t}\n\tresp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.RoleID = userCacheInfo.RoleID\n\tif resp.RoleID == role.RoleAdminID {\n\t\terr = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, userCacheInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\n// RetrievePassWord .\nfunc (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) error {\n\tuserInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\n\t// send email\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  req.Email,\n\t\tUserID: userInfo.ID,\n\t}\n\tcode := token.GenerateToken()\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/password-reset?code=%s\", us.getSiteUrl(ctx), code)\n\ttitle, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())\n\treturn nil\n}\n\n// UpdatePasswordWhenForgot update user password when user forgot password\nfunc (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {\n\tdata := &schema.EmailCodeContent{}\n\terr = data.FromJSONString(req.Content)\n\tif err != nil {\n\t\treturn errors.BadRequest(reason.EmailVerifyURLExpired)\n\t}\n\n\tuserInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\tenpass, err := us.encryptPassword(ctx, req.Pass)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// When the user changes the password, all the current user's tokens are invalid.\n\tus.authService.RemoveUserAllTokens(ctx, userInfo.ID)\n\treturn nil\n}\n\nfunc (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) {\n\tuserInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !has {\n\t\treturn false, errors.BadRequest(reason.UserNotFound)\n\t}\n\tisPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)\n\tif !isPass {\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// UserModifyPassword user modify password\nfunc (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {\n\tenpass, err := us.encryptPassword(ctx, req.Pass)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tisPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)\n\tif !isPass {\n\t\treturn errors.BadRequest(reason.OldPasswordVerificationFailed)\n\t}\n\terr = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tus.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)\n\treturn nil\n}\n\n// UpdateInfo update user info\nfunc (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (\n\terrFields []*validator.FormErrorField, err error) {\n\tif len(req.Username) > 0 {\n\t\tif checker.IsInvalidUsername(req.Username) {\n\t\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\t\tErrorField: \"username\",\n\t\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t\t}), errors.BadRequest(reason.UsernameInvalid)\n\t\t}\n\t\t// admin can use reserved username\n\t\tif !req.IsAdmin && checker.IsReservedUsername(req.Username) {\n\t\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\t\tErrorField: \"username\",\n\t\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t\t}), errors.BadRequest(reason.UsernameInvalid)\n\t\t} else if req.IsAdmin && checker.IsUsersIgnorePath(req.Username) {\n\t\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\t\tErrorField: \"username\",\n\t\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t\t}), errors.BadRequest(reason.UsernameInvalid)\n\t\t}\n\n\t\tuserInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist && userInfo.ID != req.UserID {\n\t\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\t\tErrorField: \"username\",\n\t\t\t\tErrorMsg:   reason.UsernameDuplicate,\n\t\t\t}), errors.BadRequest(reason.UsernameDuplicate)\n\t\t}\n\t}\n\n\toldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tcond := us.formatUserInfoForUpdateInfo(oldUserInfo, req)\n\n\tus.cleanUpRemovedAvatar(ctx, oldUserInfo.Avatar, cond.Avatar)\n\n\terr = us.userRepo.UpdateInfo(ctx, cond)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tus.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserUpdate, req.UserID))\n\treturn nil, err\n}\n\nfunc (us *UserService) cleanUpRemovedAvatar(\n\tctx context.Context,\n\toldAvatarJSON string,\n\tnewAvatarJSON string,\n) {\n\tif oldAvatarJSON == newAvatarJSON {\n\t\treturn\n\t}\n\n\tvar oldAvatar, newAvatar schema.AvatarInfo\n\n\t_ = json.Unmarshal([]byte(oldAvatarJSON), &oldAvatar)\n\t_ = json.Unmarshal([]byte(newAvatarJSON), &newAvatar)\n\n\tif len(oldAvatar.Custom) == 0 {\n\t\treturn\n\t}\n\n\t// clean up if old is custom and it's either removed or replaced\n\tif oldAvatar.Custom != newAvatar.Custom {\n\t\tfileRecord, err := us.fileRecordService.GetFileRecordByURL(ctx, oldAvatar.Custom)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif fileRecord == nil {\n\t\t\tlog.Warn(\"no file record found for old avatar url:\", oldAvatar.Custom)\n\t\t\treturn\n\t\t}\n\t\tif err := us.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n}\n\nfunc (us *UserService) formatUserInfoForUpdateInfo(\n\toldUserInfo *entity.User, req *schema.UpdateInfoRequest) *entity.User {\n\tavatar, _ := json.Marshal(req.Avatar)\n\n\tuserInfo := &entity.User{}\n\tuserInfo.DisplayName = oldUserInfo.DisplayName\n\tuserInfo.Username = oldUserInfo.Username\n\tuserInfo.Avatar = oldUserInfo.Avatar\n\tuserInfo.Bio = oldUserInfo.Bio\n\tuserInfo.BioHTML = oldUserInfo.BioHTML\n\tuserInfo.Website = oldUserInfo.Website\n\tuserInfo.Location = oldUserInfo.Location\n\tuserInfo.ID = req.UserID\n\n\tif len(req.DisplayName) > 0 {\n\t\tuserInfo.DisplayName = req.DisplayName\n\t}\n\tif len(req.Username) > 0 {\n\t\tuserInfo.Username = req.Username\n\t}\n\tif len(avatar) > 0 {\n\t\tuserInfo.Avatar = string(avatar)\n\t}\n\tuserInfo.Bio = req.Bio\n\tuserInfo.BioHTML = req.BioHTML\n\tuserInfo.Website = req.Website\n\tuserInfo.Location = req.Location\n\treturn userInfo\n}\n\n// UserUpdateInterface update user interface\nfunc (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) {\n\treturn us.userRepo.UpdateUserInterface(ctx, req.UserId, req.Language, req.ColorScheme)\n}\n\n// UserRegisterByEmail user register\nfunc (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (\n\tresp *schema.UserLoginResp, errFields []*validator.FormErrorField, err error,\n) {\n\t_, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif has {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"e_mail\",\n\t\t\tErrorMsg:   reason.EmailDuplicate,\n\t\t})\n\t\treturn nil, errFields, errors.BadRequest(reason.EmailDuplicate)\n\t}\n\n\tuserInfo := &entity.User{}\n\tuserInfo.EMail = registerUserInfo.Email\n\tuserInfo.DisplayName = registerUserInfo.Name\n\tuserInfo.Pass, err = us.encryptPassword(ctx, registerUserInfo.Pass)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tuserInfo.Username, err = us.userCommonService.MakeUsername(ctx, registerUserInfo.Name)\n\tif err != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"name\",\n\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t})\n\t\treturn nil, errFields, err\n\t}\n\tuserInfo.IPInfo = registerUserInfo.IP\n\tuserInfo.MailStatus = entity.EmailStatusToBeVerified\n\tuserInfo.Status = entity.UserStatusAvailable\n\tuserInfo.LastLoginDate = time.Now()\n\terr = us.userRepo.AddUser(ctx, userInfo)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{userInfo.ID}); err != nil {\n\t\tlog.Errorf(\"set default user notification config failed, err: %v\", err)\n\t}\n\n\t// send email\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  registerUserInfo.Email,\n\t\tUserID: userInfo.ID,\n\t}\n\tcode := token.GenerateToken()\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/account-activation?code=%s\", us.getSiteUrl(ctx), code)\n\ttitle, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())\n\n\troleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\t// return user info and token\n\tresp = &schema.UserLoginResp{}\n\tresp.ConvertFromUserEntity(userInfo)\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\tuserCacheInfo := &entity.UserCacheInfo{\n\t\tUserID:      userInfo.ID,\n\t\tEmailStatus: userInfo.MailStatus,\n\t\tUserStatus:  userInfo.Status,\n\t\tRoleID:      roleID,\n\t}\n\tresp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tresp.RoleID = userCacheInfo.RoleID\n\tif resp.RoleID == role.RoleAdminID {\n\t\terr = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\treturn resp, nil, nil\n}\n\nfunc (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) error {\n\tuserInfo, has, err := us.userRepo.GetByUserID(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  userInfo.EMail,\n\t\tUserID: userInfo.ID,\n\t}\n\tcode := token.GenerateToken()\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/account-activation?code=%s\", us.getSiteUrl(ctx), code)\n\ttitle, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())\n\treturn nil\n}\n\nfunc (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.UserLoginResp, err error) {\n\tdata := &schema.EmailCodeContent{}\n\terr = data.FromJSONString(req.Content)\n\tif err != nil {\n\t\treturn nil, errors.BadRequest(reason.EmailVerifyURLExpired)\n\t}\n\n\tuserInfo, has, err := us.userRepo.GetByEmail(ctx, data.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !has {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tif userInfo.MailStatus == entity.EmailStatusToBeVerified {\n\t\tuserInfo.MailStatus = entity.EmailStatusAvailable\n\t\terr = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {\n\t\tlog.Error(err)\n\t\treturn nil, err\n\t}\n\n\t// In the case of three-party login, the associated users are bound\n\tif len(data.BindingKey) > 0 {\n\t\terr = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\taccessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(\n\t\tctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &schema.UserLoginResp{}\n\tresp.ConvertFromUserEntity(userInfo)\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\tresp.AccessToken = accessToken\n\t// User verified email will update user email status. So user status cache should be updated.\n\tif err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// verifyPassword\n// Compare whether the password is correct\nfunc (us *UserService) verifyPassword(_ context.Context, loginPass, userPass string) bool {\n\tif len(loginPass) == 0 && len(userPass) == 0 {\n\t\treturn true\n\t}\n\terr := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass))\n\treturn err == nil\n}\n\n// encryptPassword\n// The password does irreversible encryption.\nfunc (us *UserService) encryptPassword(_ context.Context, pass string) (string, error) {\n\thashPwd, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)\n\t// This encrypted string can be saved to the database and can be used as password matching verification\n\treturn string(hashPwd), err\n}\n\n// UserChangeEmailSendCode user change email verification\nfunc (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) (\n\tresp []*validator.FormErrorField, err error) {\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\t// If user's email already verified, then must verify password first.\n\tif userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {\n\t\tresp = append(resp, &validator.FormErrorField{\n\t\t\tErrorField: \"pass\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),\n\t\t})\n\t\treturn resp, errors.BadRequest(reason.OldPasswordVerificationFailed)\n\t}\n\n\t_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\tresp = append([]*validator.FormErrorField{}, &validator.FormErrorField{\n\t\t\tErrorField: \"e_mail\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),\n\t\t})\n\t\treturn resp, errors.BadRequest(reason.EmailDuplicate)\n\t}\n\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  req.Email,\n\t\tUserID: req.UserID,\n\t}\n\tcode := token.GenerateToken()\n\tvar title, body string\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/confirm-new-email?code=%s\", us.getSiteUrl(ctx), code)\n\tif userInfo.MailStatus == entity.EmailStatusToBeVerified {\n\t\ttitle, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL)\n\t} else {\n\t\ttitle, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlog.Infof(\"send email confirmation %s\", verifyEmailURL)\n\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())\n\treturn nil, nil\n}\n\n// UserChangeEmailVerify user change email verify code\nfunc (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (resp *schema.UserLoginResp, err error) {\n\tdata := &schema.EmailCodeContent{}\n\terr = data.FromJSONString(content)\n\tif err != nil {\n\t\treturn nil, errors.BadRequest(reason.EmailVerifyURLExpired)\n\t}\n\n\t_, exist, err := us.userRepo.GetByEmail(ctx, data.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\treturn nil, errors.BadRequest(reason.EmailDuplicate)\n\t}\n\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\terr = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email)\n\tif err != nil {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\terr = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// if email status is to be verified, active user as well\n\tif userInfo.MailStatus == entity.EmailStatusToBeVerified {\n\t\tif err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\troleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tresp = &schema.UserLoginResp{}\n\tresp.ConvertFromUserEntity(userInfo)\n\tresp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\tuserCacheInfo := &entity.UserCacheInfo{\n\t\tUserID:      userInfo.ID,\n\t\tEmailStatus: entity.EmailStatusAvailable,\n\t\tUserStatus:  userInfo.Status,\n\t\tRoleID:      roleID,\n\t}\n\tresp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// User verified email will update user email status. So user status cache should be updated.\n\tif err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {\n\t\treturn nil, err\n\t}\n\tresp.RoleID = userCacheInfo.RoleID\n\tif resp.RoleID == role.RoleAdminID {\n\t\terr = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn resp, nil\n}\n\n// getSiteUrl get site url\nfunc (us *UserService) getSiteUrl(ctx context.Context) string {\n\tsiteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site general failed: %s\", err)\n\t\treturn \"\"\n\t}\n\treturn siteGeneral.SiteUrl\n}\n\n// UserRanking get user ranking\nfunc (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankingResp, err error) {\n\tlimit := 20\n\tendTime := time.Now()\n\tstartTime := endTime.AddDate(0, 0, -7)\n\tuserIDs, userIDExist := make([]string, 0), make(map[string]bool, 0)\n\n\t// get most reputation users\n\trankStat, rankStatUserIDs, err := us.getActivityUserRankStat(ctx, startTime, endTime, limit, userIDExist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs = append(userIDs, rankStatUserIDs...)\n\n\t// get most vote users\n\tvoteStat, voteStatUserIDs, err := us.getActivityUserVoteStat(ctx, startTime, endTime, limit, userIDExist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs = append(userIDs, voteStatUserIDs...)\n\n\t// get all staff members\n\tuserRoleRels, staffUserIDs, err := us.getStaff(ctx, userIDExist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs = append(userIDs, staffUserIDs...)\n\n\t// get user information\n\tuserInfoMapping, err := us.getUserInfoMapping(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil\n}\n\n// GetUserStaff get user staff\nfunc (us *UserService) GetUserStaff(ctx context.Context, req *schema.GetUserStaffReq) (\n\tresp []*schema.GetUserStaffResp, err error) {\n\tuserList, err := us.userRepo.SearchUserListByName(ctx, req.Username, req.PageSize, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tavatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList)\n\tfor _, u := range userList {\n\t\tresp = append(resp, &schema.GetUserStaffResp{\n\t\t\tUsername:    u.Username,\n\t\t\tDisplayName: u.DisplayName,\n\t\t\tAvatar:      avatarMapping[u.ID].GetURL(),\n\t\t})\n\t}\n\treturn resp, nil\n}\n\n// UserUnsubscribeNotification user unsubscribe email notification\nfunc (us *UserService) UserUnsubscribeNotification(\n\tctx context.Context, req *schema.UserUnsubscribeNotificationReq) (err error) {\n\tdata := &schema.EmailCodeContent{}\n\terr = data.FromJSONString(req.Content)\n\tif err != nil || len(data.UserID) == 0 {\n\t\treturn errors.BadRequest(reason.EmailVerifyURLExpired)\n\t}\n\n\tfor _, source := range data.NotificationSources {\n\t\tnotificationConfig, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource(\n\t\t\tctx, data.UserID, source)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\tcontinue\n\t\t}\n\t\tchannels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)\n\t\t// unsubscribe email notification\n\t\tfor _, channel := range channels {\n\t\t\tif channel.Key == constant.EmailChannel {\n\t\t\t\tchannel.Enable = false\n\t\t\t}\n\t\t}\n\t\tnotificationConfig.Channels = channels.ToJsonString()\n\t\tif err = us.userNotificationConfigRepo.Save(ctx, notificationConfig); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,\n\tuserIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {\n\tif plugin.RankAgentEnabled() {\n\t\treturn make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil\n\t}\n\trankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor _, stat := range rankStat {\n\t\tif stat.Rank <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif userIDExist[stat.UserID] {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, stat.UserID)\n\t\tuserIDExist[stat.UserID] = true\n\t}\n\treturn rankStat, userIDs, nil\n}\n\nfunc (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int,\n\tuserIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) {\n\tif plugin.RankAgentEnabled() {\n\t\treturn make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil\n\t}\n\tvoteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor _, stat := range voteStat {\n\t\tif stat.VoteCount <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif userIDExist[stat.UserID] {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, stat.UserID)\n\t\tuserIDExist[stat.UserID] = true\n\t}\n\treturn voteStat, userIDs, nil\n}\n\nfunc (us *UserService) getStaff(ctx context.Context, userIDExist map[string]bool) (\n\tuserRoleRels []*entity.UserRoleRel, userIDs []string, err error) {\n\tuserRoleRels, err = us.userRoleService.GetUserByRoleID(ctx, []int{role.RoleAdminID, role.RoleModeratorID})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor _, rel := range userRoleRels {\n\t\tif userIDExist[rel.UserID] {\n\t\t\tcontinue\n\t\t}\n\t\tuserIDs = append(userIDs, rel.UserID)\n\t\tuserIDExist[rel.UserID] = true\n\t}\n\treturn userRoleRels, userIDs, nil\n}\n\nfunc (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) (\n\tuserInfoMapping map[string]*entity.User, err error) {\n\tuserInfoMapping = make(map[string]*entity.User, 0)\n\tif len(userIDs) == 0 {\n\t\treturn userInfoMapping, nil\n\t}\n\tuserInfoList, err := us.userRepo.BatchGetByID(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tavatarMapping := us.siteInfoService.FormatListAvatar(ctx, userInfoList)\n\tfor _, user := range userInfoList {\n\t\tuser.Avatar = avatarMapping[user.ID].GetURL()\n\t\tuserInfoMapping[user.ID] = user\n\t}\n\treturn userInfoMapping, nil\n}\n\nfunc (us *UserService) SearchUserListByName(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (\n\tresp []*schema.UserBasicInfo, err error) {\n\tresp = make([]*schema.UserBasicInfo, 0)\n\tif len(req.Username) == 0 {\n\t\treturn resp, nil\n\t}\n\tuserList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5, false)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tavatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList)\n\tfor _, u := range userList {\n\t\tif req.UserID == u.ID {\n\t\t\tcontinue\n\t\t}\n\t\tbasicInfo := us.userCommonService.FormatUserBasicInfo(ctx, u)\n\t\tbasicInfo.Avatar = avatarMapping[u.ID].GetURL()\n\t\tresp = append(resp, basicInfo)\n\t}\n\treturn resp, nil\n}\n\nfunc (us *UserService) warpStatRankingResp(\n\tuserInfoMapping map[string]*entity.User,\n\trankStat []*entity.ActivityUserRankStat,\n\tvoteStat []*entity.ActivityUserVoteStat,\n\tuserRoleRels []*entity.UserRoleRel) (resp *schema.UserRankingResp) {\n\tresp = &schema.UserRankingResp{\n\t\tUsersWithTheMostReputation: make([]*schema.UserRankingSimpleInfo, 0),\n\t\tUsersWithTheMostVote:       make([]*schema.UserRankingSimpleInfo, 0),\n\t\tStaffs:                     make([]*schema.UserRankingSimpleInfo, 0),\n\t}\n\tfor _, stat := range rankStat {\n\t\tif stat.Rank <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted {\n\t\t\tresp.UsersWithTheMostReputation = append(resp.UsersWithTheMostReputation, &schema.UserRankingSimpleInfo{\n\t\t\t\tUsername:    userInfo.Username,\n\t\t\t\tRank:        stat.Rank,\n\t\t\t\tDisplayName: userInfo.DisplayName,\n\t\t\t\tAvatar:      userInfo.Avatar,\n\t\t\t})\n\t\t}\n\t}\n\tfor _, stat := range voteStat {\n\t\tif stat.VoteCount <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted {\n\t\t\tresp.UsersWithTheMostVote = append(resp.UsersWithTheMostVote, &schema.UserRankingSimpleInfo{\n\t\t\t\tUsername:    userInfo.Username,\n\t\t\t\tVoteCount:   stat.VoteCount,\n\t\t\t\tDisplayName: userInfo.DisplayName,\n\t\t\t\tAvatar:      userInfo.Avatar,\n\t\t\t})\n\t\t}\n\t}\n\tfor _, rel := range userRoleRels {\n\t\tif userInfo := userInfoMapping[rel.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted {\n\t\t\tresp.Staffs = append(resp.Staffs, &schema.UserRankingSimpleInfo{\n\t\t\t\tUsername:    userInfo.Username,\n\t\t\t\tRank:        userInfo.Rank,\n\t\t\t\tDisplayName: userInfo.DisplayName,\n\t\t\t\tAvatar:      userInfo.Avatar,\n\t\t\t})\n\t\t}\n\t}\n\treturn resp\n}\n"
  },
  {
    "path": "internal/service/content/vote_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage content\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/activity_type\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// VoteRepo activity repository\ntype VoteRepo interface {\n\tVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)\n\tCancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)\n\tGetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)\n\tListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (\n\t\tvoteList []*entity.Activity, total int64, err error)\n}\n\n// VoteService user service\ntype VoteService struct {\n\tvoteRepo          VoteRepo\n\tconfigService     *config.ConfigService\n\tquestionRepo      questioncommon.QuestionRepo\n\tanswerRepo        answercommon.AnswerRepo\n\tcommentCommonRepo comment_common.CommentCommonRepo\n\tobjectService     *object_info.ObjService\n\teventQueueService eventqueue.Service\n}\n\nfunc NewVoteService(\n\tvoteRepo VoteRepo,\n\tconfigService *config.ConfigService,\n\tquestionRepo questioncommon.QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\tcommentCommonRepo comment_common.CommentCommonRepo,\n\tobjectService *object_info.ObjService,\n\teventQueueService eventqueue.Service,\n) *VoteService {\n\treturn &VoteService{\n\t\tvoteRepo:          voteRepo,\n\t\tconfigService:     configService,\n\t\tquestionRepo:      questionRepo,\n\t\tanswerRepo:        answerRepo,\n\t\tcommentCommonRepo: commentCommonRepo,\n\t\tobjectService:     objectService,\n\t\teventQueueService: eventQueueService,\n\t}\n}\n\n// VoteUp vote up\nfunc (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {\n\tobjectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif objectInfo.IsDeleted() {\n\t\treturn nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)\n\t}\n\t// make object id must be decoded\n\tobjectInfo.ObjectID = req.ObjectID\n\n\t// check user is voting self or not\n\tif objectInfo.ObjectCreatorUserID == req.UserID {\n\t\treturn nil, errors.BadRequest(reason.DisallowVoteYourSelf)\n\t}\n\n\tvoteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)\n\n\t// vote operation\n\tif req.IsCancel {\n\t\terr = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)\n\t} else {\n\t\t// cancel vote down if exist\n\t\tvoteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)\n\t\terr = vs.voteRepo.CancelVote(ctx, voteOperationInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = vs.voteRepo.Vote(ctx, voteUpOperationInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &schema.VoteResp{}\n\tresp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Votes = resp.UpVotes - resp.DownVotes\n\tif !req.IsCancel {\n\t\tresp.VoteStatus = constant.ActVoteUp\n\t\tvs.sendEvent(ctx, req, objectInfo, resp)\n\t}\n\treturn resp, nil\n}\n\n// VoteDown vote down\nfunc (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {\n\tobjectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif objectInfo.IsDeleted() {\n\t\treturn nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)\n\t}\n\t// make object id must be decoded\n\tobjectInfo.ObjectID = req.ObjectID\n\n\t// check user is voting self or not\n\tif objectInfo.ObjectCreatorUserID == req.UserID {\n\t\treturn nil, errors.BadRequest(reason.DisallowVoteYourSelf)\n\t}\n\n\t// vote operation\n\tvoteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)\n\tif req.IsCancel {\n\t\terr = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\t// cancel vote up if exist\n\t\terr = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = vs.voteRepo.Vote(ctx, voteDownOperationInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tresp = &schema.VoteResp{}\n\tresp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.Votes = resp.UpVotes - resp.DownVotes\n\tif !req.IsCancel {\n\t\tresp.VoteStatus = constant.ActVoteDown\n\t\tvs.sendEvent(ctx, req, objectInfo, resp)\n\t}\n\treturn resp, nil\n}\n\n// ListUserVotes list user's votes\nfunc (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) {\n\ttypeKeys := []string{\n\t\tactivity_type.QuestionVoteUp,\n\t\tactivity_type.QuestionVoteDown,\n\t\tactivity_type.AnswerVoteUp,\n\t\tactivity_type.AnswerVoteDown,\n\t}\n\tactivityTypes := make([]int, 0)\n\tactivityTypeMapping := make(map[int]string, 0)\n\n\tfor _, typeKey := range typeKeys {\n\t\tcfg, err := vs.configService.GetConfigByKey(ctx, typeKey)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tactivityTypes = append(activityTypes, cfg.ID)\n\t\tactivityTypeMapping[cfg.ID] = typeKey\n\t}\n\n\tvoteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\n\tvotes := make([]*schema.GetVoteWithPageResp, 0)\n\tfor _, voteInfo := range voteList {\n\t\tobjInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\titem := &schema.GetVoteWithPageResp{\n\t\t\tCreatedAt:  voteInfo.CreatedAt.Unix(),\n\t\t\tObjectID:   objInfo.ObjectID,\n\t\t\tQuestionID: objInfo.QuestionID,\n\t\t\tAnswerID:   objInfo.AnswerID,\n\t\t\tObjectType: objInfo.ObjectType,\n\t\t\tTitle:      objInfo.Title,\n\t\t\tUrlTitle:   htmltext.UrlTitle(objInfo.Title),\n\t\t\tContent:    objInfo.Content,\n\t\t}\n\t\titem.VoteType = translator.Tr(lang,\n\t\t\tactivity_type.ActivityTypeFlagMapping[activityTypeMapping[voteInfo.ActivityType]])\n\t\tif objInfo.QuestionStatus == entity.QuestionStatusDeleted {\n\t\t\titem.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey)\n\t\t}\n\t\tvotes = append(votes, item)\n\t}\n\treturn pager.NewPageModel(total, votes), err\n}\n\nfunc (vs *VoteService) createVoteOperationInfo(ctx context.Context,\n\tuserID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo {\n\t// warp vote operation\n\tvoteOperationInfo := &schema.VoteOperationInfo{\n\t\tObjectID:            objectInfo.ObjectID,\n\t\tObjectType:          objectInfo.ObjectType,\n\t\tObjectCreatorUserID: objectInfo.ObjectCreatorUserID,\n\t\tOperatingUserID:     userID,\n\t\tVoteUp:              voteUp,\n\t\tVoteDown:            !voteUp,\n\t}\n\tvoteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo)\n\treturn voteOperationInfo\n}\n\nfunc (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) (\n\tactivities []*schema.VoteActivity) {\n\tactivities = make([]*schema.VoteActivity, 0)\n\n\tvar actions []string\n\tswitch op.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tif op.VoteUp {\n\t\t\tactions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp}\n\t\t} else {\n\t\t\tactions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown}\n\t\t}\n\tcase constant.AnswerObjectType:\n\t\tif op.VoteUp {\n\t\t\tactions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp}\n\t\t} else {\n\t\t\tactions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown}\n\t\t}\n\tcase constant.CommentObjectType:\n\t\tactions = []string{activity_type.CommentVoteUp}\n\t}\n\n\tfor _, action := range actions {\n\t\tt := &schema.VoteActivity{}\n\t\tcfg, err := vs.configService.GetConfigByKey(ctx, action)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"get config by key error: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tt.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()\n\n\t\tif strings.Contains(action, \"voted\") {\n\t\t\tt.ActivityUserID = op.ObjectCreatorUserID\n\t\t\tt.TriggerUserID = op.OperatingUserID\n\t\t} else {\n\t\t\tt.ActivityUserID = op.OperatingUserID\n\t\t\tt.TriggerUserID = \"0\"\n\t\t}\n\t\tactivities = append(activities, t)\n\t}\n\treturn activities\n}\n\nfunc (vs *VoteService) sendEvent(ctx context.Context,\n\treq *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) {\n\tvar event *schema.EventMsg\n\tswitch objectInfo.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tevent = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID).\n\t\t\tQID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID)\n\tcase constant.AnswerObjectType:\n\t\tevent = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID).\n\t\t\tAID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID)\n\tcase constant.CommentObjectType:\n\t\tevent = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID).\n\t\t\tCID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID)\n\tdefault:\n\t\treturn\n\t}\n\tevent.AddExtra(\"vote_up_amount\", fmt.Sprintf(\"%d\", resp.UpVotes))\n\tevent.AddExtra(\"vote_down_amount\", fmt.Sprintf(\"%d\", resp.DownVotes))\n\tvs.eventQueueService.Send(ctx, event)\n}\n"
  },
  {
    "path": "internal/service/dashboard/dashboard_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage dashboard\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"xorm.io/xorm/schemas\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/export\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/report_common\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype dashboardService struct {\n\tquestionRepo    questioncommon.QuestionRepo\n\tanswerRepo      answercommon.AnswerRepo\n\tcommentRepo     comment_common.CommentCommonRepo\n\tvoteRepo        activity_common.VoteRepo\n\tuserRepo        usercommon.UserRepo\n\treportRepo      report_common.ReportRepo\n\tconfigService   *config.ConfigService\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n\tserviceConfig   *service_config.ServiceConfig\n\treviewService   *review.ReviewService\n\trevisionRepo    revision.RevisionRepo\n\tdata            *data.Data\n}\n\nfunc NewDashboardService(\n\tquestionRepo questioncommon.QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\tcommentRepo comment_common.CommentCommonRepo,\n\tvoteRepo activity_common.VoteRepo,\n\tuserRepo usercommon.UserRepo,\n\treportRepo report_common.ReportRepo,\n\tconfigService *config.ConfigService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tserviceConfig *service_config.ServiceConfig,\n\treviewService *review.ReviewService,\n\trevisionRepo revision.RevisionRepo,\n\tdata *data.Data,\n) DashboardService {\n\treturn &dashboardService{\n\t\tquestionRepo:    questionRepo,\n\t\tanswerRepo:      answerRepo,\n\t\tcommentRepo:     commentRepo,\n\t\tvoteRepo:        voteRepo,\n\t\tuserRepo:        userRepo,\n\t\treportRepo:      reportRepo,\n\t\tconfigService:   configService,\n\t\tsiteInfoService: siteInfoService,\n\t\tserviceConfig:   serviceConfig,\n\t\treviewService:   reviewService,\n\t\trevisionRepo:    revisionRepo,\n\t\tdata:            data,\n\t}\n}\n\ntype DashboardService interface {\n\tStatistical(ctx context.Context) (resp *schema.DashboardInfo, err error)\n}\n\nfunc (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {\n\tdashboardInfo := ds.getFromCache(ctx)\n\tsecurity, err := ds.siteInfoService.GetSiteSecurity(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get general site info failed: %s\", err)\n\t\treturn dashboardInfo, nil\n\t}\n\n\tif dashboardInfo == nil {\n\t\tdashboardInfo = &schema.DashboardInfo{}\n\t\tdashboardInfo.AnswerCount = ds.answerCount(ctx)\n\t\tdashboardInfo.CommentCount = ds.commentCount(ctx)\n\t\tdashboardInfo.UserCount = ds.userCount(ctx)\n\t\tdashboardInfo.VoteCount = ds.voteCount(ctx)\n\t\tdashboardInfo.OccupyingStorageSpace = ds.calculateStorage()\n\t\tif security.CheckUpdate {\n\t\t\tdashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx)\n\t\t}\n\t\tdashboardInfo.DatabaseVersion = ds.getDatabaseInfo()\n\t\tdashboardInfo.DatabaseSize = ds.GetDatabaseSize()\n\t}\n\n\tdashboardInfo.QuestionCount = ds.questionCount(ctx)\n\tdashboardInfo.UnansweredCount = ds.unansweredQuestionCount(ctx)\n\tdashboardInfo.ResolvedCount = ds.resolvedQuestionCount(ctx)\n\n\tif dashboardInfo.QuestionCount == 0 {\n\t\tdashboardInfo.ResolvedRate = \"0.00\"\n\t\tdashboardInfo.UnansweredRate = \"0.00\"\n\t} else {\n\t\tdashboardInfo.ResolvedRate = fmt.Sprintf(\"%.2f\", float64(dashboardInfo.ResolvedCount)/float64(dashboardInfo.QuestionCount)*100)\n\t\tdashboardInfo.UnansweredRate = fmt.Sprintf(\"%.2f\", float64(dashboardInfo.UnansweredCount)/float64(dashboardInfo.QuestionCount)*100)\n\t}\n\n\tdashboardInfo.ReportCount = ds.reportCount(ctx)\n\tdashboardInfo.SMTP = ds.smtpStatus(ctx)\n\tdashboardInfo.HTTPS = ds.httpsStatus(ctx)\n\tdashboardInfo.TimeZone = ds.getTimezone(ctx)\n\tdashboardInfo.UploadingFiles = true\n\tdashboardInfo.AppStartTime = fmt.Sprintf(\"%d\", time.Now().Unix()-schema.AppStartTime.Unix())\n\tdashboardInfo.VersionInfo.Version = constant.Version\n\tdashboardInfo.VersionInfo.Revision = constant.Revision\n\tdashboardInfo.GoVersion = constant.GoVersion\n\tdashboardInfo.LoginRequired = security.LoginRequired\n\n\tds.setCache(ctx, dashboardInfo)\n\treturn dashboardInfo, nil\n}\n\nfunc (ds *dashboardService) getFromCache(ctx context.Context) (dashboardInfo *schema.DashboardInfo) {\n\tinfoStr, exist, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey)\n\tif err != nil {\n\t\tlog.Errorf(\"get dashboard statistical from cache failed: %s\", err)\n\t\treturn nil\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tdashboardInfo = &schema.DashboardInfo{}\n\tif err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {\n\t\treturn nil\n\t}\n\treturn dashboardInfo\n}\n\nfunc (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) {\n\tinfoStr, _ := json.Marshal(info)\n\terr := ds.data.Cache.SetString(ctx, schema.DashboardCacheKey, string(infoStr), schema.DashboardCacheTime)\n\tif err != nil {\n\t\tlog.Errorf(\"set dashboard statistical failed: %s\", err)\n\t}\n}\n\nfunc (ds *dashboardService) questionCount(ctx context.Context) int64 {\n\tquestionCount, err := ds.questionRepo.GetQuestionCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get question count failed: %s\", err)\n\t}\n\treturn questionCount\n}\n\nfunc (ds *dashboardService) unansweredQuestionCount(ctx context.Context) int64 {\n\tunansweredQuestionCount, err := ds.questionRepo.GetUnansweredQuestionCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get unanswered question count failed: %s\", err)\n\t}\n\treturn unansweredQuestionCount\n}\n\nfunc (ds *dashboardService) resolvedQuestionCount(ctx context.Context) int64 {\n\tresolvedQuestionCount, err := ds.questionRepo.GetResolvedQuestionCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get resolved question count failed: %s\", err)\n\t}\n\treturn resolvedQuestionCount\n}\n\nfunc (ds *dashboardService) answerCount(ctx context.Context) int64 {\n\tanswerCount, err := ds.answerRepo.GetAnswerCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get answer count failed: %s\", err)\n\t}\n\treturn answerCount\n}\n\nfunc (ds *dashboardService) commentCount(ctx context.Context) int64 {\n\tcommentCount, err := ds.commentRepo.GetCommentCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get comment count failed: %s\", err)\n\t}\n\treturn commentCount\n}\n\nfunc (ds *dashboardService) userCount(ctx context.Context) int64 {\n\tuserCount, err := ds.userRepo.GetUserCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get user count failed: %s\", err)\n\t}\n\treturn userCount\n}\n\nfunc (ds *dashboardService) reportCount(ctx context.Context) int64 {\n\treviewCount, err := ds.reviewService.GetReviewPendingCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get review count failed: %s\", err)\n\t}\n\treportCount, err := ds.reportRepo.GetReportCount(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get report count failed: %s\", err)\n\t}\n\tcountUnreviewedRevision, err := ds.revisionRepo.CountUnreviewedRevision(ctx, []int{\n\t\tconstant.ObjectTypeStrMapping[constant.AnswerObjectType],\n\t\tconstant.ObjectTypeStrMapping[constant.QuestionObjectType],\n\t\tconstant.ObjectTypeStrMapping[constant.TagObjectType],\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"get revision count failed: %s\", err)\n\t}\n\treturn reviewCount + reportCount + countUnreviewedRevision\n}\n\n// count vote\nfunc (ds *dashboardService) voteCount(ctx context.Context) int64 {\n\ttypeKeys := []string{\n\t\t\"question.vote_up\",\n\t\t\"question.vote_down\",\n\t\t\"answer.vote_up\",\n\t\t\"answer.vote_down\",\n\t}\n\tvar activityTypes []int\n\tfor _, typeKey := range typeKeys {\n\t\tcfg, err := ds.configService.GetConfigByKey(ctx, typeKey)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tactivityTypes = append(activityTypes, cfg.ID)\n\t}\n\tvoteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes)\n\tif err != nil {\n\t\tlog.Errorf(\"get vote count failed: %s\", err)\n\t}\n\treturn voteCount\n}\n\nfunc (ds *dashboardService) remoteVersion(_ context.Context) string {\n\treq, _ := http.NewRequest(\"GET\", \"https://answer.apache.org/data/latest.json?from_version=\"+constant.Version, nil)\n\treq.Header.Set(\"User-Agent\", \"Answer/\"+constant.Version)\n\thttpClient := &http.Client{}\n\thttpClient.Timeout = 15 * time.Second\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\tlog.Errorf(\"request remote version failed: %s\", err)\n\t\treturn \"\"\n\t}\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\n\trespByte, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Errorf(\"read response body failed: %s\", err)\n\t\treturn \"\"\n\t}\n\tremoteVersion := &schema.RemoteVersion{}\n\tif err := json.Unmarshal(respByte, remoteVersion); err != nil {\n\t\tlog.Errorf(\"parsing response body failed: %s\", err)\n\t\treturn \"\"\n\t}\n\treturn remoteVersion.Release.Version\n}\n\nfunc (ds *dashboardService) smtpStatus(ctx context.Context) (smtpStatus string) {\n\tsmtpStatus = \"not_configured\"\n\temailConf, err := ds.configService.GetStringValue(ctx, \"email.config\")\n\tif err != nil {\n\t\tlog.Errorf(\"get email config failed: %s\", err)\n\t\treturn \"disabled\"\n\t}\n\tec := &export.EmailConfig{}\n\terr = json.Unmarshal([]byte(emailConf), ec)\n\tif err != nil {\n\t\tlog.Errorf(\"parsing email config failed: %s\", err)\n\t\treturn \"disabled\"\n\t}\n\tif ec.SMTPHost != \"\" {\n\t\tsmtpStatus = \"enabled\"\n\t}\n\treturn smtpStatus\n}\n\nfunc (ds *dashboardService) httpsStatus(ctx context.Context) (enabled bool) {\n\tsiteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site general failed: %s\", err)\n\t\treturn false\n\t}\n\tsiteUrl, err := url.Parse(siteGeneral.SiteUrl)\n\tif err != nil {\n\t\tlog.Errorf(\"parse site url failed: %s\", err)\n\t\treturn false\n\t}\n\treturn siteUrl.Scheme == \"https\"\n}\n\nfunc (ds *dashboardService) getTimezone(ctx context.Context) string {\n\tsiteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn siteInfoInterface.TimeZone\n}\n\nfunc (ds *dashboardService) calculateStorage() string {\n\tdirSize, err := dir.DirSize(ds.serviceConfig.UploadPath)\n\tif err != nil {\n\t\tlog.Errorf(\"get upload dir size failed: %s\", err)\n\t\treturn \"\"\n\t}\n\treturn dir.FormatFileSize(dirSize)\n}\n\nfunc (ds *dashboardService) getDatabaseInfo() (versionDesc string) {\n\tdbVersion, err := ds.data.DB.DBVersion()\n\tif err != nil {\n\t\tlog.Errorf(\"get db version failed: %s\", err)\n\t} else {\n\t\tversionDesc = fmt.Sprintf(\"%s %s\", ds.data.DB.Dialect().URI().DBType, dbVersion.Number)\n\t}\n\treturn versionDesc\n}\n\nfunc (ds *dashboardService) GetDatabaseSize() (dbSize string) {\n\tswitch ds.data.DB.Dialect().URI().DBType {\n\tcase schemas.MYSQL:\n\t\tsql := fmt.Sprintf(\"SELECT SUM(DATA_LENGTH) as db_size FROM information_schema.TABLES WHERE table_schema = '%s'\",\n\t\t\tds.data.DB.Dialect().URI().DBName)\n\t\tres, err := ds.data.DB.QueryInterface(sql)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"get db size failed: %s\", err)\n\t\t} else if len(res) > 0 && res[0][\"db_size\"] != nil {\n\t\t\tdbSizeStr, _ := res[0][\"db_size\"].(string)\n\t\t\tdbSize = dir.FormatFileSize(converter.StringToInt64(dbSizeStr))\n\t\t}\n\tcase schemas.POSTGRES:\n\t\tsql := fmt.Sprintf(\"SELECT pg_database_size('%s') AS db_size\",\n\t\t\tds.data.DB.Dialect().URI().DBName)\n\t\tres, err := ds.data.DB.QueryInterface(sql)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"get db size failed: %s\", err)\n\t\t} else if len(res) > 0 && res[0][\"db_size\"] != nil {\n\t\t\tdbSizeStr, _ := res[0][\"db_size\"].(int32)\n\t\t\tdbSize = dir.FormatFileSize(int64(dbSizeStr))\n\t\t}\n\tcase schemas.SQLITE:\n\t\tdirSize, err := dir.DirSize(ds.data.DB.DataSourceName())\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get upload dir size failed: %s\", err)\n\t\t\treturn \"\"\n\t\t}\n\t\tdbSize = dir.FormatFileSize(dirSize)\n\t}\n\treturn dbSize\n}\n"
  },
  {
    "path": "internal/service/dashboard/dashboard_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage dashboard\n"
  },
  {
    "path": "internal/service/eventqueue/event_queue.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage eventqueue\n\nimport (\n\t\"github.com/apache/answer/internal/base/queue\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\ntype Service queue.Service[*schema.EventMsg]\n\nfunc NewService() Service {\n\treturn queue.New[*schema.EventMsg](\"event\", 128)\n}\n"
  },
  {
    "path": "internal/service/export/email_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage export\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"mime\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/pkg/display\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/net/context\"\n\t\"gopkg.in/gomail.v2\"\n)\n\n// EmailService kit service\ntype EmailService struct {\n\tconfigService   *config.ConfigService\n\temailRepo       EmailRepo\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n}\n\n// EmailRepo email repository\ntype EmailRepo interface {\n\tSetCode(ctx context.Context, userID, code, content string, duration time.Duration) error\n\tVerifyCode(ctx context.Context, code string) (content string, err error)\n}\n\n// NewEmailService email service\nfunc NewEmailService(\n\tconfigService *config.ConfigService,\n\temailRepo EmailRepo,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n) *EmailService {\n\treturn &EmailService{\n\t\tconfigService:   configService,\n\t\temailRepo:       emailRepo,\n\t\tsiteInfoService: siteInfoService,\n\t}\n}\n\n// EmailConfig email config\ntype EmailConfig struct {\n\tFromEmail          string `json:\"from_email\"`\n\tFromName           string `json:\"from_name\"`\n\tSMTPHost           string `json:\"smtp_host\"`\n\tSMTPPort           int    `json:\"smtp_port\"`\n\tEncryption         string `json:\"encryption\"` // \"\" SSL TLS\n\tSMTPUsername       string `json:\"smtp_username\"`\n\tSMTPPassword       string `json:\"smtp_password\"`\n\tSMTPAuthentication bool   `json:\"smtp_authentication\"`\n}\n\nfunc (e *EmailConfig) IsSSL() bool {\n\treturn e.Encryption == \"SSL\"\n}\n\nfunc (e *EmailConfig) IsTLS() bool {\n\treturn e.Encryption == \"TLS\"\n}\n\n// SaveCode save code\nfunc (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) {\n\terr := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n}\n\n// SendAndSaveCode send email and save code\nfunc (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) {\n\terr := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tes.Send(ctx, toEmailAddr, subject, body)\n}\n\n// SendAndSaveCodeWithTime send email and save code\nfunc (es *EmailService) SendAndSaveCodeWithTime(\n\tctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {\n\terr := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tes.Send(ctx, toEmailAddr, subject, body)\n}\n\n// Send email send\nfunc (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) {\n\tlog.Infof(\"try to send email to %s\", toEmailAddr)\n\tec, err := es.GetEmailConfig(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get email config failed: %s\", err)\n\t\treturn\n\t}\n\tif len(ec.SMTPHost) == 0 {\n\t\tlog.Warnf(\"smtp host is empty, skip send email\")\n\t\treturn\n\t}\n\n\tm := gomail.NewMessage()\n\tfromName := mime.QEncoding.Encode(\"utf-8\", ec.FromName)\n\tm.SetHeader(\"From\", fmt.Sprintf(\"%s <%s>\", fromName, ec.FromEmail))\n\tm.SetHeader(\"To\", toEmailAddr)\n\tm.SetHeader(\"Subject\", subject)\n\tm.SetBody(\"text/html\", body)\n\n\td := gomail.NewDialer(ec.SMTPHost, ec.SMTPPort, ec.SMTPUsername, ec.SMTPPassword)\n\tif ec.IsSSL() {\n\t\td.SSL = true\n\t}\n\tif ec.IsTLS() {\n\t\td.SSL = false\n\t}\n\tif len(os.Getenv(\"SKIP_SMTP_TLS_VERIFY\")) > 0 {\n\t\td.TLSConfig = &tls.Config{ServerName: d.Host, InsecureSkipVerify: true}\n\t}\n\tif err := d.DialAndSend(m); err != nil {\n\t\tlog.Errorf(\"send email to %s failed: %s\", toEmailAddr, err)\n\t} else {\n\t\tlog.Infof(\"send email to %s success\", toEmailAddr)\n\t}\n}\n\n// VerifyUrlExpired email send\nfunc (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) {\n\tcontent, err := es.emailRepo.VerifyCode(ctx, code)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn content\n}\n\nfunc (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.RegisterTemplateData{\n\t\tSiteName:    siteInfo.Name,\n\t\tRegisterUrl: registerUrl,\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyRegisterTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyRegisterBody, templateData)\n\treturn title, body, nil\n}\n\nfunc (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttemplateData := &schema.PassResetTemplateData{SiteName: siteInfo.Name, PassResetUrl: passResetUrl}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyPassResetTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyPassResetBody, templateData)\n\treturn title, body, nil\n}\n\nfunc (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.ChangeEmailTemplateData{\n\t\tSiteName:       siteInfo.Name,\n\t\tChangeEmailUrl: changeEmailUrl,\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailBody, templateData)\n\treturn title, body, nil\n}\n\n// TestTemplate send test email template parse\nfunc (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.TestTemplateData{SiteName: siteInfo.Name}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyTestTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyTestBody, templateData)\n\treturn title, body, nil\n}\n\n// NewAnswerTemplate new answer template\nfunc (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (\n\ttitle, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tseoInfo, err := es.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.NewAnswerTemplateData{\n\t\tSiteName:       siteInfo.Name,\n\t\tDisplayName:    raw.AnswerUserDisplayName,\n\t\tQuestionTitle:  raw.QuestionTitle,\n\t\tAnswerUrl:      display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID),\n\t\tAnswerSummary:  raw.AnswerSummary,\n\t\tUnsubscribeUrl: fmt.Sprintf(\"%s/users/unsubscribe?code=%s\", siteInfo.SiteUrl, raw.UnsubscribeCode),\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData)\n\treturn title, body, nil\n}\n\n// NewInviteAnswerTemplate new invite answer template\nfunc (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) (\n\ttitle, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tseoInfo, err := es.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.NewInviteAnswerTemplateData{\n\t\tSiteName:       siteInfo.Name,\n\t\tDisplayName:    raw.InviterDisplayName,\n\t\tQuestionTitle:  raw.QuestionTitle,\n\t\tInviteUrl:      display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle),\n\t\tUnsubscribeUrl: fmt.Sprintf(\"%s/users/unsubscribe?code=%s\", siteInfo.SiteUrl, raw.UnsubscribeCode),\n\t}\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData)\n\treturn title, body, nil\n}\n\n// NewCommentTemplate new comment template\nfunc (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (\n\ttitle, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tseoInfo, err := es.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.NewCommentTemplateData{\n\t\tSiteName:       siteInfo.Name,\n\t\tDisplayName:    raw.CommentUserDisplayName,\n\t\tQuestionTitle:  raw.QuestionTitle,\n\t\tCommentSummary: raw.CommentSummary,\n\t\tUnsubscribeUrl: fmt.Sprintf(\"%s/users/unsubscribe?code=%s\", siteInfo.SiteUrl, raw.UnsubscribeCode),\n\t}\n\ttemplateData.CommentUrl = display.CommentURL(seoInfo.Permalink,\n\t\tsiteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID, raw.CommentID)\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData)\n\treturn title, body, nil\n}\n\n// NewQuestionTemplate new question template\nfunc (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) (\n\ttitle, body string, err error) {\n\tsiteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\tseoInfo, err := es.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\ttemplateData := &schema.NewQuestionTemplateData{\n\t\tSiteName:       siteInfo.Name,\n\t\tQuestionTitle:  raw.QuestionTitle,\n\t\tTags:           strings.Join(raw.Tags, \", \"),\n\t\tUnsubscribeUrl: fmt.Sprintf(\"%s/users/unsubscribe?code=%s\", siteInfo.SiteUrl, raw.UnsubscribeCode),\n\t}\n\ttemplateData.QuestionUrl = display.QuestionURL(\n\t\tseoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle)\n\n\tlang := handler.GetLangByCtx(ctx)\n\ttitle = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData)\n\tbody = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData)\n\treturn title, body, nil\n}\n\nfunc (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, err error) {\n\temailConf, err := es.configService.GetStringValue(ctx, constant.EmailConfigKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tec = &EmailConfig{}\n\terr = json.Unmarshal([]byte(emailConf), ec)\n\tif err != nil {\n\t\tlog.Errorf(\"old email config format is invalid, you need to update smtp config: %v\", err)\n\t\treturn nil, errors.BadRequest(reason.SiteInfoConfigNotFound)\n\t}\n\treturn ec, nil\n}\n\n// SetEmailConfig set email config\nfunc (es *EmailService) SetEmailConfig(ctx context.Context, ec *EmailConfig) (err error) {\n\tdata, _ := json.Marshal(ec)\n\treturn es.configService.UpdateConfig(ctx, constant.EmailConfigKey, string(data))\n}\n"
  },
  {
    "path": "internal/service/feature_toggle/feature_toggle_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage feature_toggle\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// Feature keys\nconst (\n\tFeatureBadge        = \"badge\"\n\tFeatureCustomDomain = \"custom_domain\"\n\tFeatureMCP          = \"mcp\"\n\tFeaturePrivateAPI   = \"private_api\"\n\tFeatureAIChatbot    = \"ai_chatbot\"\n\tFeatureArticle      = \"article\"\n\tFeatureCategory     = \"category\"\n)\n\ntype toggleConfig struct {\n\tToggles map[string]bool `json:\"toggles\"`\n}\n\n// FeatureToggleService persist and query feature switches.\ntype FeatureToggleService struct {\n\tsiteInfoRepo siteinfo_common.SiteInfoRepo\n}\n\n// NewFeatureToggleService creates a new feature toggle service instance.\nfunc NewFeatureToggleService(siteInfoRepo siteinfo_common.SiteInfoRepo) *FeatureToggleService {\n\treturn &FeatureToggleService{\n\t\tsiteInfoRepo: siteInfoRepo,\n\t}\n}\n\n// UpdateAll overwrites the feature toggle configuration.\nfunc (s *FeatureToggleService) UpdateAll(ctx context.Context, toggles map[string]bool) error {\n\tcfg := &toggleConfig{\n\t\tToggles: sanitizeToggleMap(toggles),\n\t}\n\n\tdata, err := json.Marshal(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinfo := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeFeatureToggle,\n\t\tContent: string(data),\n\t\tStatus:  1,\n\t}\n\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeFeatureToggle, info)\n}\n\n// GetAll returns all feature toggles.\nfunc (s *FeatureToggleService) GetAll(ctx context.Context) (map[string]bool, error) {\n\tsiteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeFeatureToggle, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist || siteInfo == nil || siteInfo.Content == \"\" {\n\t\treturn map[string]bool{}, nil\n\t}\n\n\tcfg := &toggleConfig{}\n\tif err := json.Unmarshal([]byte(siteInfo.Content), cfg); err != nil {\n\t\treturn map[string]bool{}, err\n\t}\n\treturn sanitizeToggleMap(cfg.Toggles), nil\n}\n\n// IsEnabled returns whether a feature is enabled. Missing config defaults to true.\nfunc (s *FeatureToggleService) IsEnabled(ctx context.Context, feature string) (bool, error) {\n\ttoggles, err := s.GetAll(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif len(toggles) == 0 {\n\t\treturn true, nil\n\t}\n\tvalue, ok := toggles[feature]\n\tif !ok {\n\t\treturn true, nil\n\t}\n\treturn value, nil\n}\n\n// EnsureEnabled returns error if feature disabled.\nfunc (s *FeatureToggleService) EnsureEnabled(ctx context.Context, feature string) error {\n\tenabled, err := s.IsEnabled(ctx, feature)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !enabled {\n\t\treturn errors.BadRequest(reason.ErrFeatureDisabled)\n\t}\n\treturn nil\n}\n\nfunc sanitizeToggleMap(in map[string]bool) map[string]bool {\n\tif in == nil {\n\t\treturn map[string]bool{}\n\t}\n\treturn in\n}\n"
  },
  {
    "path": "internal/service/file_record/file_record_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage file_record\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/pkg/writer\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// FileRecordRepo file record repository\ntype FileRecordRepo interface {\n\tAddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error)\n\tUpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error)\n\tGetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) (\n\t\tfileRecordList []*entity.FileRecord, total int64, err error)\n\tDeleteFileRecord(ctx context.Context, id int) (err error)\n\tGetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error)\n}\n\n// FileRecordService file record service\ntype FileRecordService struct {\n\tfileRecordRepo  FileRecordRepo\n\trevisionRepo    revision.RevisionRepo\n\tserviceConfig   *service_config.ServiceConfig\n\tsiteInfoService siteinfo_common.SiteInfoCommonService\n\tuserService     *usercommon.UserCommon\n}\n\n// NewFileRecordService new file record service\nfunc NewFileRecordService(\n\tfileRecordRepo FileRecordRepo,\n\trevisionRepo revision.RevisionRepo,\n\tserviceConfig *service_config.ServiceConfig,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tuserService *usercommon.UserCommon,\n) *FileRecordService {\n\treturn &FileRecordService{\n\t\tfileRecordRepo:  fileRecordRepo,\n\t\trevisionRepo:    revisionRepo,\n\t\tserviceConfig:   serviceConfig,\n\t\tsiteInfoService: siteInfoService,\n\t\tuserService:     userService,\n\t}\n}\n\n// AddFileRecord add file record\nfunc (fs *FileRecordService) AddFileRecord(ctx context.Context, userID, filePath, fileURL, source string) {\n\trecord := &entity.FileRecord{\n\t\tUserID:   userID,\n\t\tFilePath: filePath,\n\t\tFileURL:  fileURL,\n\t\tSource:   source,\n\t\tStatus:   entity.FileRecordStatusAvailable,\n\t\tObjectID: \"0\",\n\t}\n\tif err := fs.fileRecordRepo.AddFileRecord(ctx, record); err != nil {\n\t\tlog.Errorf(\"add file record error: %v\", err)\n\t}\n}\n\n// CleanOrphanUploadFiles clean orphan upload files\nfunc (fs *FileRecordService) CleanOrphanUploadFiles(ctx context.Context) {\n\tpage, pageSize := 1, 1000\n\n\tfor {\n\t\tfileRecordList, total, err := fs.fileRecordRepo.GetFileRecordPage(ctx, page, pageSize, &entity.FileRecord{\n\t\t\tStatus: entity.FileRecordStatusAvailable,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get file record page error: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif len(fileRecordList) == 0 || total == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfor _, fileRecord := range fileRecordList {\n\t\t\t// If this file record created in 48 hours, no need to check\n\t\t\tif fileRecord.CreatedAt.AddDate(0, 0, 2).After(time.Now()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isBrandingOrAvatarFile(fileRecord.FilePath) {\n\t\t\t\tif strings.Contains(fileRecord.FilePath, constant.BrandingSubPath+\"/\") {\n\t\t\t\t\tif fs.siteInfoService.IsBrandingFileUsed(ctx, fileRecord.FilePath) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t} else if strings.Contains(fileRecord.FilePath, constant.AvatarSubPath+\"/\") {\n\t\t\t\t\tif fs.userService.IsAvatarFileUsed(ctx, fileRecord.FilePath) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil {\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif checker.IsNotZeroString(fileRecord.ObjectID) {\n\t\t\t\t_, exist, err := fs.revisionRepo.GetLastRevisionByObjectID(ctx, fileRecord.ObjectID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"get last revision by object id error: %v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif exist {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlastRevision, exist, err := fs.revisionRepo.GetLastRevisionByFileURL(ctx, fileRecord.FileURL)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"get last revision by file url error: %v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif exist {\n\t\t\t\t\t// update the file record object id\n\t\t\t\t\tfileRecord.ObjectID = lastRevision.ObjectID\n\t\t\t\t\tif err := fs.fileRecordRepo.UpdateFileRecord(ctx, fileRecord); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"update file record object id error: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Delete and move the file record\n\t\t\tif err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}\n\t\tpage++\n\t}\n}\n\nfunc isBrandingOrAvatarFile(filePath string) bool {\n\treturn strings.Contains(filePath, constant.BrandingSubPath+\"/\") || strings.Contains(filePath, constant.AvatarSubPath+\"/\")\n}\n\nfunc (fs *FileRecordService) PurgeDeletedFiles(ctx context.Context) {\n\tdeletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath)\n\tlog.Infof(\"purge deleted files: %s\", deletedPath)\n\terr := os.RemoveAll(deletedPath)\n\tif err != nil {\n\t\tlog.Errorf(\"purge deleted files error: %v\", err)\n\t\treturn\n\t}\n\terr = dir.CreateDirIfNotExist(deletedPath)\n\tif err != nil {\n\t\tlog.Errorf(\"create deleted directory error: %v\", err)\n\t}\n}\n\nfunc (fs *FileRecordService) DeleteAndMoveFileRecord(ctx context.Context, fileRecord *entity.FileRecord) error {\n\t// Delete the file record\n\tif err := fs.fileRecordRepo.DeleteFileRecord(ctx, fileRecord.ID); err != nil {\n\t\treturn fmt.Errorf(\"delete file record error: %v\", err)\n\t}\n\n\t// Move the file to the deleted directory\n\toldFilename := filepath.Base(fileRecord.FilePath)\n\toldFilePath := filepath.Join(fs.serviceConfig.UploadPath, fileRecord.FilePath)\n\tdeletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath, oldFilename)\n\n\tif err := writer.MoveFile(oldFilePath, deletedPath); err != nil {\n\t\treturn fmt.Errorf(\"move file error: %v\", err)\n\t}\n\n\tlog.Debugf(\"delete and move file: %s\", fileRecord.FileURL)\n\treturn nil\n}\n\nfunc (fs *FileRecordService) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) {\n\trecord, err = fs.fileRecordRepo.GetFileRecordByURL(ctx, fileURL)\n\tif err != nil {\n\t\tlog.Errorf(\"error retrieving file record by URL: %v\", err)\n\t\treturn\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/service/follow/follow_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage follow\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n)\n\ntype FollowRepo interface {\n\tFollow(ctx context.Context, objectId, userId string) error\n\tFollowCancel(ctx context.Context, objectId, userId string) error\n}\n\ntype FollowService struct {\n\ttagRepo          tagcommon.TagCommonRepo\n\tfollowRepo       FollowRepo\n\tfollowCommonRepo activity_common.FollowRepo\n}\n\nfunc NewFollowService(\n\tfollowRepo FollowRepo,\n\tfollowCommonRepo activity_common.FollowRepo,\n\ttagRepo tagcommon.TagCommonRepo,\n) *FollowService {\n\treturn &FollowService{\n\t\tfollowRepo:       followRepo,\n\t\tfollowCommonRepo: followCommonRepo,\n\t\ttagRepo:          tagRepo,\n\t}\n}\n\n// Follow or cancel follow object\nfunc (fs *FollowService) Follow(ctx context.Context, dto *schema.FollowDTO) (resp schema.FollowResp, err error) {\n\tif dto.IsCancel {\n\t\terr = fs.followRepo.FollowCancel(ctx, dto.ObjectID, dto.UserID)\n\t} else {\n\t\terr = fs.followRepo.Follow(ctx, dto.ObjectID, dto.UserID)\n\t}\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tfollows, err := fs.followCommonRepo.GetFollowAmount(ctx, dto.ObjectID)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tresp.Follows = follows\n\tresp.IsFollowed = !dto.IsCancel\n\treturn resp, nil\n}\n\n// UpdateFollowTags update user follow tags\nfunc (fs *FollowService) UpdateFollowTags(ctx context.Context, req *schema.UpdateFollowTagsReq) (err error) {\n\tobjIDs, err := fs.followCommonRepo.GetFollowIDs(ctx, req.UserID, entity.Tag{}.TableName())\n\tif err != nil {\n\t\treturn\n\t}\n\toldFollowTagList, err := fs.tagRepo.GetTagListByIDs(ctx, objIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldTagMapping := make(map[string]bool)\n\tfor _, tag := range oldFollowTagList {\n\t\toldTagMapping[tag.SlugName] = true\n\t}\n\n\tnewTagMapping := make(map[string]bool)\n\tfor _, tag := range req.SlugNameList {\n\t\tnewTagMapping[tag] = true\n\t}\n\n\t// cancel follow\n\tfor _, tag := range oldFollowTagList {\n\t\tif !newTagMapping[tag.SlugName] {\n\t\t\terr := fs.followRepo.FollowCancel(ctx, tag.ID, req.UserID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// new follow\n\tfor _, tagSlugName := range req.SlugNameList {\n\t\tif !oldTagMapping[tagSlugName] {\n\t\t\ttagInfo, exist, err := fs.tagRepo.GetTagBySlugName(ctx, tagSlugName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !exist {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = fs.followRepo.Follow(ctx, tagInfo.ID, req.UserID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/importer/importer_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage importer\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// ImporterService importer service\ntype ImporterService struct {\n\tquestionService *content.QuestionService\n\trankService     *rank.RankService\n\tuserCommon      *usercommon.UserCommon\n}\n\n// NewRankService new rank service\nfunc NewImporterService(\n\tquestionService *content.QuestionService,\n\trankService *rank.RankService,\n\tuserCommon *usercommon.UserCommon) *ImporterService {\n\treturn &ImporterService{\n\t\tquestionService: questionService,\n\t\trankService:     rankService,\n\t\tuserCommon:      userCommon,\n\t}\n}\n\ntype ImporterFunc struct {\n\timporterService *ImporterService\n}\n\nfunc (ipfunc *ImporterFunc) AddQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) {\n\treturn ipfunc.importerService.ImportQuestion(ctx, questionInfo)\n}\n\nfunc (ip *ImporterService) NewImporterFunc() plugin.ImporterFunc {\n\treturn &ImporterFunc{importerService: ip}\n}\n\nfunc (ip *ImporterService) ImportQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) {\n\treq := &schema.QuestionAdd{}\n\terrFields := make([]*validator.FormErrorField, 0)\n\t// To limit rate, remove the following code from comment: Part 1/2\n\t// reject, rejectKey := ipc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req)\n\t// if reject {\n\t// \treturn\n\t// }\n\tuserInfo, exist, err := ip.userCommon.GetByEmail(ctx, questionInfo.UserEmail)\n\tif err != nil {\n\t\tlog.Errorf(\"error: %v\", err)\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn fmt.Errorf(\"user not found\")\n\t}\n\n\t// To limit rate, remove the following code from comment: Part 2/2\n\t// defer func() {\n\t// \t// If status is not 200 means that the bad request has been returned, so the record should be cleared\n\t// \tif ctx.Writer.Status() != http.StatusOK {\n\t// \t\tipc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey)\n\t// \t}\n\t// }()\n\treq.UserID = userInfo.ID\n\treq.Title = questionInfo.Title\n\treq.Content = questionInfo.Content\n\treq.HTML = \"<p>\" + questionInfo.Content + \"</p>\"\n\treq.Tags = make([]*schema.TagItem, len(questionInfo.Tags))\n\tfor i, tag := range questionInfo.Tags {\n\t\treq.Tags[i] = &schema.TagItem{\n\t\t\tSlugName:    tag,\n\t\t\tDisplayName: tag,\n\t\t}\n\t}\n\tcanList, requireRanks, err := ip.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{\n\t\tpermission.QuestionAdd,\n\t\tpermission.QuestionEdit,\n\t\tpermission.QuestionDelete,\n\t\tpermission.QuestionClose,\n\t\tpermission.QuestionReopen,\n\t\tpermission.TagUseReservedTag,\n\t\tpermission.TagAdd,\n\t\tpermission.LinkUrlLimit,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"error: %v\", err)\n\t\treturn err\n\t}\n\treq.CanAdd = canList[0]\n\treq.CanEdit = canList[1]\n\treq.CanDelete = canList[2]\n\treq.CanClose = canList[3]\n\treq.CanReopen = canList[4]\n\treq.CanUseReservedTag = canList[5]\n\treq.CanAddTag = canList[6]\n\tif !req.CanAdd {\n\t\tlog.Errorf(\"error: %v\", err)\n\t\treturn err\n\t}\n\thasNewTag, err := ip.questionService.HasNewTag(ctx.(*gin.Context), req.Tags)\n\tif err != nil {\n\t\tlog.Errorf(\"error: %v\", err)\n\t\treturn err\n\t}\n\tif !req.CanAddTag && hasNewTag {\n\t\tlang := handler.GetLangByCtx(ctx.(*gin.Context))\n\t\tmsg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]})\n\t\tlog.Errorf(\"error: %v\", msg)\n\t\treturn errors.BadRequest(msg)\n\t}\n\n\terrList, err := ip.questionService.CheckAddQuestion(ctx, req)\n\tif err != nil {\n\t\terrlist, ok := errList.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\tif len(errFields) > 0 {\n\t\treturn errors.BadRequest(reason.RequestFormatError)\n\t}\n\tginCtx := ctx.(*gin.Context)\n\treq.UserAgent = ginCtx.GetHeader(\"User-Agent\")\n\treq.IP = ginCtx.ClientIP()\n\tresp, err := ip.questionService.AddQuestion(ctx, req)\n\tif err != nil {\n\t\terrlist, ok := resp.([]*validator.FormErrorField)\n\t\tif ok {\n\t\t\terrFields = append(errFields, errlist...)\n\t\t}\n\t}\n\n\tif len(errFields) > 0 {\n\t\tlog.Errorf(\"error: RequestFormatError\")\n\t\treturn errors.BadRequest(reason.RequestFormatError)\n\t}\n\tlog.Info(\"Add Question Successfully\")\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/meta/meta_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage meta\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/obj\"\n\tmyErrors \"github.com/segmentfault/pacman/errors\"\n)\n\n// MetaService user service\ntype MetaService struct {\n\tmetaCommonService *metacommon.MetaCommonService\n\tuserCommon        *usercommon.UserCommon\n\tquestionRepo      questioncommon.QuestionRepo\n\tanswerRepo        answercommon.AnswerRepo\n\teventQueueService eventqueue.Service\n}\n\nfunc NewMetaService(\n\tmetaCommonService *metacommon.MetaCommonService,\n\tuserCommon *usercommon.UserCommon,\n\tanswerRepo answercommon.AnswerRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\teventQueueService eventqueue.Service,\n) *MetaService {\n\treturn &MetaService{\n\t\tmetaCommonService: metaCommonService,\n\t\tquestionRepo:      questionRepo,\n\t\tuserCommon:        userCommon,\n\t\tanswerRepo:        answerRepo,\n\t\teventQueueService: eventQueueService,\n\t}\n}\n\n// GetReactionByObjectId get reaction\nfunc (ms *MetaService) GetReactionByObjectId(ctx context.Context, req *schema.GetReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) {\n\treactionMeta, err := ms.metaCommonService.GetMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey)\n\n\t// if not exist, return nil\n\tif err != nil {\n\t\tvar pacmanErr *myErrors.Error\n\t\tif errors.As(err, &pacmanErr) && pacmanErr.Reason == reason.MetaObjectNotFound {\n\t\t\treturn nil, nil\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar reaction schema.ReactionsSummaryMeta\n\terr = json.Unmarshal([]byte(reactionMeta.Value), &reaction)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ms.convertToReactionResp(ctx, req.UserID, &reaction)\n}\n\n// AddOrUpdateReaction add or update reaction\nfunc (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.UpdateReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) {\n\t// check if object exist and it's answer or question\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(req.ObjectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar event *schema.EventMsg\n\tswitch objectType {\n\tcase constant.AnswerObjectType:\n\t\tanswerInfo, exist, err := ms.answerRepo.GetAnswer(ctx, req.ObjectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, myErrors.BadRequest(reason.AnswerNotFound)\n\t\t}\n\t\tevent = schema.NewEvent(constant.EventAnswerReact, req.UserID).TID(answerInfo.ID).\n\t\t\tAID(answerInfo.ID, answerInfo.UserID)\n\tcase constant.QuestionObjectType:\n\t\tquestionInfo, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, myErrors.BadRequest(reason.QuestionNotFound)\n\t\t}\n\t\tevent = schema.NewEvent(constant.EventQuestionReact, req.UserID).TID(questionInfo.ID).\n\t\t\tQID(questionInfo.ID, questionInfo.UserID)\n\tdefault:\n\t\treturn nil, myErrors.BadRequest(reason.ObjectNotFound)\n\t}\n\n\t// add or update\n\treactions := &schema.ReactionsSummaryMeta{}\n\terr = ms.metaCommonService.AddOrUpdateMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey, func(meta *entity.Meta, exist bool) (*entity.Meta, error) {\n\t\t// if not exist, create new one\n\t\tif exist {\n\t\t\t_ = json.Unmarshal([]byte(meta.Value), reactions)\n\t\t}\n\n\t\t// update reaction\n\t\tms.updateReaction(req, reactions)\n\n\t\t// write back to meta repo\n\t\treactSumBytes, err := json.Marshal(reactions)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &entity.Meta{\n\t\t\tObjectID: req.ObjectID,\n\t\t\tKey:      entity.ObjectReactSummaryKey,\n\t\t\tValue:    string(reactSumBytes),\n\t\t}, nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, myErrors.InternalServer(reason.DatabaseError).WithError(err)\n\t}\n\n\tresp, err = ms.convertToReactionResp(ctx, req.UserID, reactions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tms.eventQueueService.Send(ctx, event)\n\treturn resp, nil\n}\n\n// updateReaction update reaction\nfunc (ms *MetaService) updateReaction(req *schema.UpdateReactionReq, reactions *schema.ReactionsSummaryMeta) {\n\tswitch req.Reaction {\n\tcase \"activate\":\n\t\treactions.AddReactionSummary(req.Emoji, req.UserID)\n\tcase \"deactivate\":\n\t\treactions.RemoveReactionSummary(req.Emoji, req.UserID)\n\t}\n}\n\nfunc (ms *MetaService) convertToReactionResp(ctx context.Context, userId string, r *schema.ReactionsSummaryMeta) (\n\tresp *schema.GetReactionByObjectIdResp, err error) {\n\tlang := handler.GetLangByCtx(ctx)\n\tresp = &schema.GetReactionByObjectIdResp{\n\t\tReactionSummary: make([]*schema.ReactionRespItem, 0),\n\t}\n\t// traverse map and convert to username\n\tfor _, reaction := range r.Reactions {\n\t\titem := &schema.ReactionRespItem{\n\t\t\tEmoji:    reaction.Emoji,\n\t\t\tIsActive: r.CheckUserInReactionSummary(reaction.Emoji, userId),\n\t\t}\n\n\t\tusernames := make([]string, 0)\n\t\tuserBasicInfos, err := ms.userCommon.BatchUserBasicInfoByID(ctx, reaction.UserIDs)\n\t\titem.Count = len(userBasicInfos)\n\t\tif err != nil {\n\t\t\treturn resp, err\n\t\t}\n\t\t// get username\n\t\tfor _, userBasicInfo := range userBasicInfos {\n\t\t\tusernames = append(usernames, userBasicInfo.Username)\n\t\t\tif len(usernames) == 5 && len(userBasicInfos) > 5 {\n\t\t\t\titem.Tooltip = translator.TrWithData(lang, constant.ReactionTooltipLabel, map[string]string{\n\t\t\t\t\t\"Count\": strconv.Itoa(len(userBasicInfos) - 5),\n\t\t\t\t\t\"Names\": strings.Join(usernames, \", \"),\n\t\t\t\t})\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(userBasicInfos) <= 5 {\n\t\t\titem.Tooltip = strings.Join(usernames, \", \")\n\t\t}\n\t\tresp.ReactionSummary = append(resp.ReactionSummary, item)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/meta_common/meta_common_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage metacommon\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\tmyErrors \"github.com/segmentfault/pacman/errors\"\n)\n\n// MetaRepo meta repository\ntype MetaRepo interface {\n\tAddMeta(ctx context.Context, meta *entity.Meta) (err error)\n\tRemoveMeta(ctx context.Context, id int) (err error)\n\tUpdateMeta(ctx context.Context, meta *entity.Meta) (err error)\n\tAddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error\n\tGetMetaByObjectIdAndKey(ctx context.Context, objectId, key string) (meta *entity.Meta, exist bool, err error)\n\tGetMetaList(ctx context.Context, meta *entity.Meta) (metas []*entity.Meta, err error)\n}\n\n// MetaCommonService user service\ntype MetaCommonService struct {\n\tmetaRepo MetaRepo\n}\n\nfunc NewMetaCommonService(metaRepo MetaRepo) *MetaCommonService {\n\treturn &MetaCommonService{\n\t\tmetaRepo: metaRepo,\n\t}\n}\n\n// AddMeta add meta\nfunc (ms *MetaCommonService) AddMeta(ctx context.Context, objID, key, value string) (err error) {\n\tmeta := &entity.Meta{\n\t\tObjectID: objID,\n\t\tKey:      key,\n\t\tValue:    value,\n\t}\n\treturn ms.metaRepo.AddMeta(ctx, meta)\n}\n\n// RemoveMeta delete meta\nfunc (ms *MetaCommonService) RemoveMeta(ctx context.Context, id int) (err error) {\n\treturn ms.metaRepo.RemoveMeta(ctx, id)\n}\n\n// UpdateMeta update meta\nfunc (ms *MetaCommonService) UpdateMeta(ctx context.Context, metaID int, key, value string) (err error) {\n\tmeta := &entity.Meta{\n\t\tID:    metaID,\n\t\tKey:   key,\n\t\tValue: value,\n\t}\n\treturn ms.metaRepo.UpdateMeta(ctx, meta)\n}\n\nfunc (ms *MetaCommonService) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objID, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) (err error) {\n\treturn ms.metaRepo.AddOrUpdateMetaByObjectIdAndKey(ctx, objID, key, f)\n}\n\n// GetMetaByObjectIdAndKey get meta one\nfunc (ms *MetaCommonService) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (meta *entity.Meta, err error) {\n\tmeta, exist, err := ms.metaRepo.GetMetaByObjectIdAndKey(ctx, objectID, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, myErrors.BadRequest(reason.MetaObjectNotFound)\n\t}\n\treturn meta, nil\n}\n\n// GetMetaList get meta list all\nfunc (ms *MetaCommonService) GetMetaList(ctx context.Context, objID string) (metas []*entity.Meta, err error) {\n\tmetas, err = ms.metaRepo.GetMetaList(ctx, &entity.Meta{ObjectID: objID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn metas, err\n}\n"
  },
  {
    "path": "internal/service/mock/siteinfo_repo_mock.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Code generated by MockGen. DO NOT EDIT.\n// Source: ./siteinfo_service.go\n//\n// Generated by this command:\n//\n//\tmockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock\n//\n\n// Package mock is a generated GoMock package.\npackage mock\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tentity \"github.com/apache/answer/internal/entity\"\n\tschema \"github.com/apache/answer/internal/schema\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockSiteInfoRepo is a mock of SiteInfoRepo interface.\ntype MockSiteInfoRepo struct {\n\tctrl     *gomock.Controller\n\trecorder *MockSiteInfoRepoMockRecorder\n\tisgomock struct{}\n}\n\n// MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo.\ntype MockSiteInfoRepoMockRecorder struct {\n\tmock *MockSiteInfoRepo\n}\n\n// NewMockSiteInfoRepo creates a new mock instance.\nfunc NewMockSiteInfoRepo(ctrl *gomock.Controller) *MockSiteInfoRepo {\n\tmock := &MockSiteInfoRepo{ctrl: ctrl}\n\tmock.recorder = &MockSiteInfoRepoMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockSiteInfoRepo) EXPECT() *MockSiteInfoRepoMockRecorder {\n\treturn m.recorder\n}\n\n// GetByType mocks base method.\nfunc (m *MockSiteInfoRepo) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (*entity.SiteInfo, bool, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, siteType}\n\tfor _, a := range withoutCache {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"GetByType\", varargs...)\n\tret0, _ := ret[0].(*entity.SiteInfo)\n\tret1, _ := ret[1].(bool)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// GetByType indicates an expected call of GetByType.\nfunc (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType any, withoutCache ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, siteType}, withoutCache...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetByType\", reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), varargs...)\n}\n\n// IsBrandingFileUsed mocks base method.\nfunc (m *MockSiteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"IsBrandingFileUsed\", ctx, filePath)\n\tret0, _ := ret[0].(bool)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed.\nfunc (mr *MockSiteInfoRepoMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IsBrandingFileUsed\", reflect.TypeOf((*MockSiteInfoRepo)(nil).IsBrandingFileUsed), ctx, filePath)\n}\n\n// SaveByType mocks base method.\nfunc (m *MockSiteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SaveByType\", ctx, siteType, data)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SaveByType indicates an expected call of SaveByType.\nfunc (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SaveByType\", reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data)\n}\n\n// MockSiteInfoCommonService is a mock of SiteInfoCommonService interface.\ntype MockSiteInfoCommonService struct {\n\tctrl     *gomock.Controller\n\trecorder *MockSiteInfoCommonServiceMockRecorder\n\tisgomock struct{}\n}\n\n// MockSiteInfoCommonServiceMockRecorder is the mock recorder for MockSiteInfoCommonService.\ntype MockSiteInfoCommonServiceMockRecorder struct {\n\tmock *MockSiteInfoCommonService\n}\n\n// NewMockSiteInfoCommonService creates a new mock instance.\nfunc NewMockSiteInfoCommonService(ctrl *gomock.Controller) *MockSiteInfoCommonService {\n\tmock := &MockSiteInfoCommonService{ctrl: ctrl}\n\tmock.recorder = &MockSiteInfoCommonServiceMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockSiteInfoCommonService) EXPECT() *MockSiteInfoCommonServiceMockRecorder {\n\treturn m.recorder\n}\n\n// FormatAvatar mocks base method.\nfunc (m *MockSiteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"FormatAvatar\", ctx, originalAvatarData, email, userStatus)\n\tret0, _ := ret[0].(*schema.AvatarInfo)\n\treturn ret0\n}\n\n// FormatAvatar indicates an expected call of FormatAvatar.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, originalAvatarData, email, userStatus any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FormatAvatar\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatAvatar), ctx, originalAvatarData, email, userStatus)\n}\n\n// FormatListAvatar mocks base method.\nfunc (m *MockSiteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) map[string]*schema.AvatarInfo {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"FormatListAvatar\", ctx, userList)\n\tret0, _ := ret[0].(map[string]*schema.AvatarInfo)\n\treturn ret0\n}\n\n// FormatListAvatar indicates an expected call of FormatListAvatar.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, userList any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FormatListAvatar\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatListAvatar), ctx, userList)\n}\n\n// GetSiteAI mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteAI(ctx context.Context) (*schema.SiteAIResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteAI\", ctx)\n\tret0, _ := ret[0].(*schema.SiteAIResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteAI indicates an expected call of GetSiteAI.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteAI(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteAI\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteAI), ctx)\n}\n\n// GetSiteAdvanced mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteAdvanced(ctx context.Context) (*schema.SiteAdvancedResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteAdvanced\", ctx)\n\tret0, _ := ret[0].(*schema.SiteAdvancedResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteAdvanced indicates an expected call of GetSiteAdvanced.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteAdvanced(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteAdvanced\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteAdvanced), ctx)\n}\n\n// GetSiteBranding mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteBranding(ctx context.Context) (*schema.SiteBrandingResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteBranding\", ctx)\n\tret0, _ := ret[0].(*schema.SiteBrandingResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteBranding indicates an expected call of GetSiteBranding.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteBranding\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteBranding), ctx)\n}\n\n// GetSiteCustomCssHTML mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (*schema.SiteCustomCssHTMLResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteCustomCssHTML\", ctx)\n\tret0, _ := ret[0].(*schema.SiteCustomCssHTMLResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteCustomCssHTML indicates an expected call of GetSiteCustomCssHTML.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteCustomCssHTML\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteCustomCssHTML), ctx)\n}\n\n// GetSiteGeneral mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteGeneral(ctx context.Context) (*schema.SiteGeneralResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteGeneral\", ctx)\n\tret0, _ := ret[0].(*schema.SiteGeneralResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteGeneral indicates an expected call of GetSiteGeneral.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteGeneral\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteGeneral), ctx)\n}\n\n// GetSiteInfoByType mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteInfoByType\", ctx, siteType, resp)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// GetSiteInfoByType indicates an expected call of GetSiteInfoByType.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, siteType, resp any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteInfoByType\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInfoByType), ctx, siteType, resp)\n}\n\n// GetSiteInterface mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteInterface(ctx context.Context) (*schema.SiteInterfaceSettingsResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteInterface\", ctx)\n\tret0, _ := ret[0].(*schema.SiteInterfaceSettingsResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteInterface indicates an expected call of GetSiteInterface.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteInterface\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInterface), ctx)\n}\n\n// GetSiteLogin mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteLogin(ctx context.Context) (*schema.SiteLoginResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteLogin\", ctx)\n\tret0, _ := ret[0].(*schema.SiteLoginResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteLogin indicates an expected call of GetSiteLogin.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteLogin\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLogin), ctx)\n}\n\n// GetSiteMCP mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteMCP(ctx context.Context) (*schema.SiteMCPResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteMCP\", ctx)\n\tret0, _ := ret[0].(*schema.SiteMCPResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteMCP indicates an expected call of GetSiteMCP.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteMCP(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteMCP\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteMCP), ctx)\n}\n\n// GetSitePolicies mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSitePolicies(ctx context.Context) (*schema.SitePoliciesResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSitePolicies\", ctx)\n\tret0, _ := ret[0].(*schema.SitePoliciesResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSitePolicies indicates an expected call of GetSitePolicies.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSitePolicies(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSitePolicies\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSitePolicies), ctx)\n}\n\n// GetSiteQuestion mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteQuestion(ctx context.Context) (*schema.SiteQuestionsResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteQuestion\", ctx)\n\tret0, _ := ret[0].(*schema.SiteQuestionsResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteQuestion indicates an expected call of GetSiteQuestion.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteQuestion(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteQuestion\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteQuestion), ctx)\n}\n\n// GetSiteSecurity mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteSecurity(ctx context.Context) (*schema.SiteSecurityResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteSecurity\", ctx)\n\tret0, _ := ret[0].(*schema.SiteSecurityResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteSecurity indicates an expected call of GetSiteSecurity.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSecurity(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteSecurity\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSecurity), ctx)\n}\n\n// GetSiteSeo mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteSeo(ctx context.Context) (*schema.SiteSeoResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteSeo\", ctx)\n\tret0, _ := ret[0].(*schema.SiteSeoResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteSeo indicates an expected call of GetSiteSeo.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteSeo\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSeo), ctx)\n}\n\n// GetSiteTag mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteTag(ctx context.Context) (*schema.SiteTagsResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteTag\", ctx)\n\tret0, _ := ret[0].(*schema.SiteTagsResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteTag indicates an expected call of GetSiteTag.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTag(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteTag\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTag), ctx)\n}\n\n// GetSiteTheme mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteTheme(ctx context.Context) (*schema.SiteThemeResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteTheme\", ctx)\n\tret0, _ := ret[0].(*schema.SiteThemeResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteTheme indicates an expected call of GetSiteTheme.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteTheme\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTheme), ctx)\n}\n\n// GetSiteUsers mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteUsers(ctx context.Context) (*schema.SiteUsersResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteUsers\", ctx)\n\tret0, _ := ret[0].(*schema.SiteUsersResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteUsers indicates an expected call of GetSiteUsers.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteUsers\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsers), ctx)\n}\n\n// GetSiteUsersSettings mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteUsersSettings(ctx context.Context) (*schema.SiteUsersSettingsResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteUsersSettings\", ctx)\n\tret0, _ := ret[0].(*schema.SiteUsersSettingsResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteUsersSettings indicates an expected call of GetSiteUsersSettings.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsersSettings(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteUsersSettings\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsersSettings), ctx)\n}\n\n// GetSiteWrite mocks base method.\nfunc (m *MockSiteInfoCommonService) GetSiteWrite(ctx context.Context) (*schema.SiteWriteResp, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetSiteWrite\", ctx)\n\tret0, _ := ret[0].(*schema.SiteWriteResp)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetSiteWrite indicates an expected call of GetSiteWrite.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetSiteWrite\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteWrite), ctx)\n}\n\n// IsBrandingFileUsed mocks base method.\nfunc (m *MockSiteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"IsBrandingFileUsed\", ctx, filePath)\n\tret0, _ := ret[0].(bool)\n\treturn ret0\n}\n\n// IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed.\nfunc (mr *MockSiteInfoCommonServiceMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IsBrandingFileUsed\", reflect.TypeOf((*MockSiteInfoCommonService)(nil).IsBrandingFileUsed), ctx, filePath)\n}\n"
  },
  {
    "path": "internal/service/noticequeue/notice_queue.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage noticequeue\n\nimport (\n\t\"github.com/apache/answer/internal/base/queue\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\ntype Service queue.Service[*schema.NotificationMsg]\n\nfunc NewService() Service {\n\treturn queue.New[*schema.NotificationMsg](\"notification\", 128)\n}\n\ntype ExternalService queue.Service[*schema.ExternalNotificationMsg]\n\nfunc NewExternalService() ExternalService {\n\treturn queue.New[*schema.ExternalNotificationMsg](\"external_notification\", 128)\n}\n"
  },
  {
    "path": "internal/service/notification/external_notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype ExternalNotificationService struct {\n\tdata                       *data.Data\n\tuserNotificationConfigRepo user_notification_config.UserNotificationConfigRepo\n\tfollowRepo                 activity_common.FollowRepo\n\temailService               *export.EmailService\n\tuserRepo                   usercommon.UserRepo\n\tnotificationQueueService   noticequeue.ExternalService\n\tuserExternalLoginRepo      user_external_login.UserExternalLoginRepo\n\tsiteInfoService            siteinfo_common.SiteInfoCommonService\n}\n\nfunc NewExternalNotificationService(\n\tdata *data.Data,\n\tuserNotificationConfigRepo user_notification_config.UserNotificationConfigRepo,\n\tfollowRepo activity_common.FollowRepo,\n\temailService *export.EmailService,\n\tuserRepo usercommon.UserRepo,\n\tnotificationQueueService noticequeue.ExternalService,\n\tuserExternalLoginRepo user_external_login.UserExternalLoginRepo,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n) *ExternalNotificationService {\n\tn := &ExternalNotificationService{\n\t\tdata:                       data,\n\t\tuserNotificationConfigRepo: userNotificationConfigRepo,\n\t\tfollowRepo:                 followRepo,\n\t\temailService:               emailService,\n\t\tuserRepo:                   userRepo,\n\t\tnotificationQueueService:   notificationQueueService,\n\t\tuserExternalLoginRepo:      userExternalLoginRepo,\n\t\tsiteInfoService:            siteInfoService,\n\t}\n\tnotificationQueueService.RegisterHandler(n.Handler)\n\treturn n\n}\n\nfunc (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.ExternalNotificationMsg) error {\n\tlog.Debugf(\"try to send external notification %+v\", msg)\n\n\t// If receiver not set language, use site default language.\n\tif len(msg.ReceiverLang) == 0 || msg.ReceiverLang == translator.DefaultLangOption {\n\t\tif interfaceInfo, _ := ns.siteInfoService.GetSiteInterface(ctx); interfaceInfo != nil {\n\t\t\tmsg.ReceiverLang = interfaceInfo.Language\n\t\t}\n\t}\n\tif msg.NewQuestionTemplateRawData != nil {\n\t\treturn ns.handleNewQuestionNotification(ctx, msg)\n\t}\n\tif msg.NewCommentTemplateRawData != nil {\n\t\treturn ns.handleNewCommentNotification(ctx, msg)\n\t}\n\tif msg.NewAnswerTemplateRawData != nil {\n\t\treturn ns.handleNewAnswerNotification(ctx, msg)\n\t}\n\tif msg.NewInviteAnswerTemplateRawData != nil {\n\t\treturn ns.handleInviteAnswerNotification(ctx, msg)\n\t}\n\tlog.Errorf(\"unknown notification message: %+v\", msg)\n\treturn nil\n}\n\nfunc (ns *ExternalNotificationService) checkUserStatusBeforeNotification(ctx context.Context, userID string) (\n\tunavailable bool) {\n\tuserInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"get user %s info error: %v\", userID, err)\n\t\treturn true\n\t}\n\tif !exist || userInfo.Status != entity.UserStatusAvailable {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/service/notification/invite_answer_notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc (ns *ExternalNotificationService) handleInviteAnswerNotification(ctx context.Context,\n\tmsg *schema.ExternalNotificationMsg) error {\n\tlog.Debugf(\"try to send invite answer notification %+v\", msg)\n\n\tnotificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tchannels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)\n\tfor _, channel := range channels {\n\t\tif !channel.Enable {\n\t\t\tcontinue\n\t\t}\n\t\tif channel.Key == constant.EmailChannel {\n\t\t\tns.sendInviteAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewInviteAnswerTemplateRawData)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx context.Context,\n\tuserID, email, lang string, rawData *schema.NewInviteAnswerTemplateRawData) {\n\tif unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable {\n\t\treturn\n\t}\n\tcodeContent := &schema.EmailCodeContent{\n\t\tSourceType: schema.UnsubscribeSourceType,\n\t\tNotificationSources: []constant.NotificationSource{\n\t\t\tconstant.InboxSource,\n\t\t},\n\t\tEmail:                    email,\n\t\tUserID:                   userID,\n\t\tSkipValidationLatestCode: true,\n\t}\n\n\t// If receiver has set language, use it to send email.\n\tif len(lang) > 0 {\n\t\tctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang))\n\t}\n\ttitle, body, err := ns.emailService.NewInviteAnswerTemplate(ctx, rawData)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tns.emailService.SendAndSaveCodeWithTime(\n\t\tctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)\n}\n"
  },
  {
    "path": "internal/service/notification/new_answer_notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc (ns *ExternalNotificationService) handleNewAnswerNotification(ctx context.Context,\n\tmsg *schema.ExternalNotificationMsg) error {\n\tlog.Debugf(\"try to send new comment notification %+v\", msg)\n\n\tnotificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tchannels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)\n\tfor _, channel := range channels {\n\t\tif !channel.Enable {\n\t\t\tcontinue\n\t\t}\n\t\tif channel.Key == constant.EmailChannel {\n\t\t\tns.sendNewAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewAnswerTemplateRawData)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx context.Context,\n\tuserID, email, lang string, rawData *schema.NewAnswerTemplateRawData) {\n\tif unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable {\n\t\treturn\n\t}\n\tcodeContent := &schema.EmailCodeContent{\n\t\tSourceType: schema.UnsubscribeSourceType,\n\t\tNotificationSources: []constant.NotificationSource{\n\t\t\tconstant.InboxSource,\n\t\t},\n\t\tEmail:                    email,\n\t\tUserID:                   userID,\n\t\tSkipValidationLatestCode: true,\n\t}\n\n\t// If receiver has set language, use it to send email.\n\tif len(lang) > 0 {\n\t\tctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang))\n\t}\n\ttitle, body, err := ns.emailService.NewAnswerTemplate(ctx, rawData)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tns.emailService.SendAndSaveCodeWithTime(\n\t\tctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)\n}\n"
  },
  {
    "path": "internal/service/notification/new_comment_notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc (ns *ExternalNotificationService) handleNewCommentNotification(ctx context.Context,\n\tmsg *schema.ExternalNotificationMsg) error {\n\tlog.Debugf(\"try to send new comment notification %+v\", msg)\n\n\tnotificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tchannels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)\n\tfor _, channel := range channels {\n\t\tif !channel.Enable {\n\t\t\tcontinue\n\t\t}\n\t\tif channel.Key == constant.EmailChannel {\n\t\t\tns.sendNewCommentNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewCommentTemplateRawData)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx context.Context,\n\tuserID, email, lang string, rawData *schema.NewCommentTemplateRawData) {\n\tif unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable {\n\t\treturn\n\t}\n\tcodeContent := &schema.EmailCodeContent{\n\t\tSourceType: schema.UnsubscribeSourceType,\n\t\tNotificationSources: []constant.NotificationSource{\n\t\t\tconstant.InboxSource,\n\t\t},\n\t\tEmail:                    email,\n\t\tUserID:                   userID,\n\t\tSkipValidationLatestCode: true,\n\t}\n\t// If receiver has set language, use it to send email.\n\tif len(lang) > 0 {\n\t\tctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(lang))\n\t}\n\ttitle, body, err := ns.emailService.NewCommentTemplate(ctx, rawData)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tns.emailService.SendAndSaveCodeWithTime(\n\t\tctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)\n}\n"
  },
  {
    "path": "internal/service/notification/new_question_notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/display\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype NewQuestionSubscriber struct {\n\tUserID             string                      `json:\"user_id\"`\n\tChannels           schema.NotificationChannels `json:\"channels\"`\n\tNotificationSource constant.NotificationSource `json:\"notification_source\"`\n}\n\nfunc (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context.Context,\n\tmsg *schema.ExternalNotificationMsg) error {\n\tlog.Debugf(\"try to send new question notification %+v\", msg)\n\tsubscribers, err := ns.getNewQuestionSubscribers(ctx, msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"get subscribers %d for question %s\", len(subscribers), msg.NewQuestionTemplateRawData.QuestionID)\n\n\tfor _, subscriber := range subscribers {\n\t\tfor _, channel := range subscriber.Channels {\n\t\t\tif !channel.Enable {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif channel.Key == constant.EmailChannel {\n\t\t\t\tns.sendNewQuestionNotificationEmail(ctx, subscriber.UserID, &schema.NewQuestionTemplateRawData{\n\t\t\t\t\tQuestionTitle:   msg.NewQuestionTemplateRawData.QuestionTitle,\n\t\t\t\t\tQuestionID:      msg.NewQuestionTemplateRawData.QuestionID,\n\t\t\t\t\tUnsubscribeCode: token.GenerateToken(),\n\t\t\t\t\tTags:            msg.NewQuestionTemplateRawData.Tags,\n\t\t\t\t\tTagIDs:          msg.NewQuestionTemplateRawData.TagIDs,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tns.syncNewQuestionNotificationToPlugin(ctx, msg)\n\treturn nil\n}\n\nfunc (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Context, msg *schema.ExternalNotificationMsg) (\n\tsubscribers []*NewQuestionSubscriber, err error) {\n\tsubscribersMapping := make(map[string]*NewQuestionSubscriber)\n\n\t// 1. get all this new question's tags followers\n\ttagsFollowerIDs := make([]string, 0)\n\tfollowerMapping := make(map[string]bool)\n\tfor _, tagID := range msg.NewQuestionTemplateRawData.TagIDs {\n\t\tuserIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, userID := range userIDs {\n\t\t\tif _, ok := followerMapping[userID]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfollowerMapping[userID] = true\n\t\t\ttagsFollowerIDs = append(tagsFollowerIDs, userID)\n\t\t}\n\t}\n\tuserNotificationConfigs, err := ns.userNotificationConfigRepo.GetByUsersAndSource(\n\t\tctx, tagsFollowerIDs, constant.AllNewQuestionForFollowingTagsSource)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, userNotificationConfig := range userNotificationConfigs {\n\t\tif _, ok := subscribersMapping[userNotificationConfig.UserID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tsubscribersMapping[userNotificationConfig.UserID] = &NewQuestionSubscriber{\n\t\t\tUserID:             userNotificationConfig.UserID,\n\t\t\tChannels:           schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels),\n\t\t\tNotificationSource: constant.AllNewQuestionForFollowingTagsSource,\n\t\t}\n\t}\n\tlog.Debugf(\"get %d subscribers from tags\", len(subscribersMapping))\n\n\t// 2. get all new question's followers\n\tnotificationConfigs, err := ns.userNotificationConfigRepo.GetBySource(ctx, constant.AllNewQuestionSource)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, notificationConfig := range notificationConfigs {\n\t\tif _, ok := subscribersMapping[notificationConfig.UserID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tif ns.checkSendNewQuestionNotificationEmailLimit(ctx, notificationConfig.UserID) {\n\t\t\tcontinue\n\t\t}\n\t\tsubscribersMapping[notificationConfig.UserID] = &NewQuestionSubscriber{\n\t\t\tUserID:             notificationConfig.UserID,\n\t\t\tChannels:           schema.NewNotificationChannelsFormJson(notificationConfig.Channels),\n\t\t\tNotificationSource: constant.AllNewQuestionSource,\n\t\t}\n\t}\n\n\t// 3. remove question owner\n\tdelete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID)\n\tfor _, subscriber := range subscribersMapping {\n\t\tsubscribers = append(subscribers, subscriber)\n\t}\n\tlog.Debugf(\"get %d subscribers from all new question config\", len(subscribers))\n\treturn subscribers, nil\n}\n\nfunc (ns *ExternalNotificationService) checkSendNewQuestionNotificationEmailLimit(ctx context.Context, userID string) bool {\n\tkey := constant.NewQuestionNotificationLimitCacheKeyPrefix + userID\n\told, exist, err := ns.data.Cache.GetInt64(ctx, key)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false\n\t}\n\tif exist && old >= constant.NewQuestionNotificationLimitMax {\n\t\tlog.Debugf(\"%s user reach new question notification limit\", userID)\n\t\treturn true\n\t}\n\tif !exist {\n\t\terr = ns.data.Cache.SetInt64(ctx, key, 1, constant.NewQuestionNotificationLimitCacheTime)\n\t} else {\n\t\t_, err = ns.data.Cache.Increase(ctx, key, 1)\n\t}\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn false\n}\n\nfunc (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx context.Context,\n\tuserID string, rawData *schema.NewQuestionTemplateRawData) {\n\tif unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable {\n\t\treturn\n\t}\n\tuserInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Errorf(\"user %s not exist\", userID)\n\t\treturn\n\t}\n\t// If receiver has set language, use it to send email.\n\tif len(userInfo.Language) > 0 {\n\t\tctx = context.WithValue(ctx, constant.AcceptLanguageContextKey, i18n.Language(userInfo.Language))\n\t}\n\ttitle, body, err := ns.emailService.NewQuestionTemplate(ctx, rawData)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tcodeContent := &schema.EmailCodeContent{\n\t\tSourceType: schema.UnsubscribeSourceType,\n\t\tEmail:      userInfo.EMail,\n\t\tUserID:     userID,\n\t\tNotificationSources: []constant.NotificationSource{\n\t\t\tconstant.AllNewQuestionSource,\n\t\t\tconstant.AllNewQuestionForFollowingTagsSource,\n\t\t},\n\t\tSkipValidationLatestCode: true,\n\t}\n\tns.emailService.SendAndSaveCodeWithTime(\n\t\tctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)\n}\n\nfunc (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context,\n\tmsg *schema.ExternalNotificationMsg) {\n\t_ = plugin.CallNotification(func(fn plugin.Notification) error {\n\t\t// 1. get all this new question's tags followers\n\t\tsubscribersMapping := make(map[string]plugin.NotificationType)\n\t\tfor _, tagID := range msg.NewQuestionTemplateRawData.TagIDs {\n\t\t\tuserIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, userID := range userIDs {\n\t\t\t\tsubscribersMapping[userID] = plugin.NotificationNewQuestionFollowedTag\n\t\t\t}\n\t\t}\n\n\t\t// 2. get all new question's followers\n\t\tquestionSubscribers := fn.GetNewQuestionSubscribers()\n\t\tfor _, subscriber := range questionSubscribers {\n\t\t\tsubscribersMapping[subscriber] = plugin.NotificationNewQuestion\n\t\t}\n\n\t\t// 3. remove question owner\n\t\tdelete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID)\n\n\t\tpluginNotificationMsg := ns.newPluginQuestionNotification(ctx, msg)\n\n\t\t// 4. send notification\n\t\tfor subscriberUserID, notificationType := range subscribersMapping {\n\t\t\tnewMsg := plugin.NotificationMessage{}\n\t\t\t_ = copier.Copy(&newMsg, pluginNotificationMsg)\n\t\t\tnewMsg.ReceiverUserID = subscriberUserID\n\t\t\tnewMsg.Type = notificationType\n\n\t\t\tif len(subscriberUserID) > 0 {\n\t\t\t\tuserInfo, _, _ := ns.userRepo.GetByUserID(ctx, subscriberUserID)\n\t\t\t\tif userInfo != nil && len(userInfo.Language) > 0 && userInfo.Language != translator.DefaultLangOption {\n\t\t\t\t\tnewMsg.ReceiverLang = userInfo.Language\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get all external logins as fallback\n\t\t\texternalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, subscriberUserID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"get user external login list failed for user %s: %v\", subscriberUserID, err)\n\t\t\t} else if len(externalLogins) > 0 {\n\t\t\t\tnewMsg.ReceiverExternalID = externalLogins[0].ExternalID\n\t\t\t\tif len(externalLogins) > 1 {\n\t\t\t\t\tlog.Debugf(\"user %s has %d SSO logins, using most recent: provider=%s\",\n\t\t\t\t\t\tsubscriberUserID, len(externalLogins), externalLogins[0].Provider)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Try to get external login specific to this plugin (takes precedence over fallback)\n\t\t\tuserInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, subscriberUserID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"get user external login info failed: %v\", err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\tnewMsg.ReceiverExternalID = userInfo.ExternalID\n\t\t\t}\n\t\t\tfn.Notify(newMsg)\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (ns *ExternalNotificationService) newPluginQuestionNotification(\n\tctx context.Context, msg *schema.ExternalNotificationMsg) (raw *plugin.NotificationMessage) {\n\traw = &plugin.NotificationMessage{\n\t\tReceiverUserID: msg.ReceiverUserID,\n\t\tReceiverLang:   msg.ReceiverLang,\n\t\tQuestionTitle:  msg.NewQuestionTemplateRawData.QuestionTitle,\n\t\tQuestionTags:   strings.Join(msg.NewQuestionTemplateRawData.Tags, \",\"),\n\t}\n\tsiteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn raw\n\t}\n\tseoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\treturn raw\n\t}\n\tinterfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\treturn raw\n\t}\n\tif len(raw.ReceiverLang) == 0 || raw.ReceiverLang == translator.DefaultLangOption {\n\t\traw.ReceiverLang = interfaceInfo.Language\n\t}\n\traw.QuestionUrl = display.QuestionURL(\n\t\tseoInfo.Permalink, siteInfo.SiteUrl,\n\t\tmsg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle)\n\tif len(msg.NewQuestionTemplateRawData.QuestionAuthorUserID) > 0 {\n\t\ttriggerUser, exist, err := ns.userRepo.GetByUserID(ctx, msg.NewQuestionTemplateRawData.QuestionAuthorUserID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get trigger user basic info failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif exist {\n\t\t\traw.TriggerUserID = triggerUser.ID\n\t\t\traw.TriggerUserDisplayName = triggerUser.DisplayName\n\t\t\traw.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)\n\t\t}\n\t}\n\treturn raw\n}\n"
  },
  {
    "path": "internal/service/notification/notification_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notification\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\tnotficationcommon \"github.com/apache/answer/internal/service/notification_common\"\n\t\"github.com/apache/answer/internal/service/report_common\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// NotificationService user service\ntype NotificationService struct {\n\tdata               *data.Data\n\tnotificationRepo   notficationcommon.NotificationRepo\n\tnotificationCommon *notficationcommon.NotificationCommon\n\trevisionService    *revision_common.RevisionService\n\treportRepo         report_common.ReportRepo\n\treviewService      *review.ReviewService\n\tuserRepo           usercommon.UserRepo\n\tbadgeRepo          badge.BadgeRepo\n}\n\nfunc NewNotificationService(\n\tdata *data.Data,\n\tnotificationRepo notficationcommon.NotificationRepo,\n\tnotificationCommon *notficationcommon.NotificationCommon,\n\trevisionService *revision_common.RevisionService,\n\tuserRepo usercommon.UserRepo,\n\treportRepo report_common.ReportRepo,\n\treviewService *review.ReviewService,\n\tbadgeRepo badge.BadgeRepo,\n) *NotificationService {\n\treturn &NotificationService{\n\t\tdata:               data,\n\t\tnotificationRepo:   notificationRepo,\n\t\tnotificationCommon: notificationCommon,\n\t\trevisionService:    revisionService,\n\t\tuserRepo:           userRepo,\n\t\treportRepo:         reportRepo,\n\t\treviewService:      reviewService,\n\t\tbadgeRepo:          badgeRepo,\n\t}\n}\n\nfunc (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) {\n\tinboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID)\n\tachievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID)\n\n\tredBot := &schema.RedDot{}\n\tredBot.Inbox, _, _ = ns.data.Cache.GetInt64(ctx, inboxKey)\n\tredBot.Achievement, _, _ = ns.data.Cache.GetInt64(ctx, achievementKey)\n\n\t// get review amount\n\tif req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag {\n\t\tredBot.CanRevision = true\n\t\tredBot.Revision = ns.countAllReviewAmount(ctx, req)\n\t}\n\n\t// get badge award\n\tredBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID)\n\treturn redBot, nil\n}\n\nfunc (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) {\n\tkey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)\n\tcacheData, exist, err := ns.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\tlog.Errorf(\"get badge award failed: %v\", err)\n\t\treturn nil\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\n\tc := schema.NewRedDotBadgeAwardCache()\n\tc.FromJSON(cacheData)\n\taward := c.GetBadgeAward()\n\tif award == nil {\n\t\treturn nil\n\t}\n\tbadgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID)\n\tif err != nil {\n\t\tlog.Errorf(\"get badge info failed: %v\", err)\n\t\treturn nil\n\t}\n\tif !exists {\n\t\treturn nil\n\t}\n\taward.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name)\n\taward.Icon = badgeInfo.Icon\n\taward.Level = badgeInfo.Level\n\treturn award\n}\n\nfunc (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) {\n\t// get queue amount\n\tif req.IsAdmin {\n\t\treviewCount, err := ns.reviewService.GetReviewPendingCount(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get report count failed: %v\", err)\n\t\t} else {\n\t\t\tamount += reviewCount\n\t\t}\n\t}\n\n\t// get flag amount\n\tif req.IsAdmin {\n\t\treportCount, err := ns.reportRepo.GetReportCount(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get report count failed: %v\", err)\n\t\t} else {\n\t\t\tamount += reportCount\n\t\t}\n\t}\n\n\t// get suggestion amount\n\tcountUnreviewedRevision, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, &schema.RevisionSearch{\n\t\tCanReviewQuestion: req.CanReviewQuestion,\n\t\tCanReviewAnswer:   req.CanReviewAnswer,\n\t\tCanReviewTag:      req.CanReviewTag,\n\t\tUserID:            req.UserID,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"get unreviewed revision count failed: %v\", err)\n\t} else {\n\t\tamount += countUnreviewedRevision\n\t}\n\treturn amount\n}\n\nfunc (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) {\n\t_ = ns.notificationCommon.DeleteRedDot(ctx, req.UserID, schema.NotificationType[req.NotificationType])\n\tresp := &schema.GetRedDot{}\n\t_ = copier.Copy(resp, req)\n\treturn ns.GetRedDot(ctx, resp)\n}\n\nfunc (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error {\n\tbotType, ok := schema.NotificationType[notificationType]\n\tif ok {\n\t\terr := ns.notificationRepo.ClearUnRead(ctx, userID, botType)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error {\n\tnotificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id)\n\tif err != nil {\n\t\tlog.Errorf(\"get notification failed: %v\", err)\n\t\treturn nil\n\t}\n\tif !exist || notificationInfo.UserID != userID {\n\t\treturn nil\n\t}\n\tif notificationInfo.IsRead == schema.NotificationNotRead {\n\t\terr := ns.notificationRepo.ClearIDUnRead(ctx, userID, id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id)\n\tif err != nil {\n\t\tlog.Errorf(\"remove badge award alert cache failed: %v\", err)\n\t}\n\n\t_ = ns.notificationCommon.DecreaseRedDot(ctx, userID, notificationInfo.Type)\n\treturn nil\n}\n\nfunc (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) (\n\tpageModel *pager.PageModel, err error) {\n\tresp := make([]*schema.NotificationContent, 0)\n\tsearchType, ok := schema.NotificationType[searchCond.TypeStr]\n\tif !ok {\n\t\treturn pager.NewPageModel(0, resp), nil\n\t}\n\tsearchInboxType := schema.NotificationInboxTypeAll\n\tif searchType == schema.NotificationTypeInbox {\n\t\t_, ok = schema.NotificationInboxType[searchCond.InboxTypeStr]\n\t\tif ok {\n\t\t\tsearchInboxType = schema.NotificationInboxType[searchCond.InboxTypeStr]\n\t\t}\n\t}\n\tsearchCond.Type = searchType\n\tsearchCond.InboxType = searchInboxType\n\tnotifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = ns.formatNotificationPage(ctx, notifications)\n\treturn pager.NewPageModel(total, resp), nil\n}\n\nfunc (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (\n\tresp []*schema.NotificationContent) {\n\tlang := handler.GetLangByCtx(ctx)\n\tenableShortID := handler.GetEnableShortID(ctx)\n\tuserIDs := make([]string, 0)\n\tuserMapping := make(map[string]bool)\n\tfor _, notificationInfo := range notifications {\n\t\titem := &schema.NotificationContent{}\n\t\tif err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {\n\t\t\tlog.Error(\"NotificationContent Unmarshal Error\", err.Error())\n\t\t\tcontinue\n\t\t}\n\t\t// If notification is downvote, the user info is not needed.\n\t\tif item.NotificationAction == constant.NotificationDownVotedTheQuestion ||\n\t\t\titem.NotificationAction == constant.NotificationDownVotedTheAnswer {\n\t\t\titem.UserInfo = nil\n\t\t}\n\t\t// If notification is badge, the user info is not needed and the title need to be translated.\n\t\tif item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType {\n\t\t\tbadgeName := translator.Tr(lang, item.ObjectInfo.Title)\n\t\t\titem.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct {\n\t\t\t\tBadgeName string\n\t\t\t}{BadgeName: badgeName})\n\t\t\titem.UserInfo = nil\n\t\t}\n\n\t\titem.ID = notificationInfo.ID\n\t\titem.NotificationAction = translator.Tr(lang, item.NotificationAction)\n\t\titem.UpdateTime = notificationInfo.UpdatedAt.Unix()\n\t\titem.IsRead = notificationInfo.IsRead == schema.NotificationRead\n\n\t\tif enableShortID {\n\t\t\tif answerID, ok := item.ObjectInfo.ObjectMap[\"answer\"]; ok {\n\t\t\t\tif item.ObjectInfo.ObjectID == answerID {\n\t\t\t\t\titem.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap[\"answer\"])\n\t\t\t\t}\n\t\t\t\titem.ObjectInfo.ObjectMap[\"answer\"] = uid.EnShortID(item.ObjectInfo.ObjectMap[\"answer\"])\n\t\t\t}\n\t\t\tif questionID, ok := item.ObjectInfo.ObjectMap[\"question\"]; ok {\n\t\t\t\tif item.ObjectInfo.ObjectID == questionID {\n\t\t\t\t\titem.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap[\"question\"])\n\t\t\t\t}\n\t\t\t\titem.ObjectInfo.ObjectMap[\"question\"] = uid.EnShortID(item.ObjectInfo.ObjectMap[\"question\"])\n\t\t\t}\n\t\t}\n\n\t\tif item.UserInfo != nil && !userMapping[item.UserInfo.ID] {\n\t\t\tuserIDs = append(userIDs, item.UserInfo.ID)\n\t\t\tuserMapping[item.UserInfo.ID] = true\n\t\t}\n\t\tresp = append(resp, item)\n\t}\n\n\tif len(userIDs) == 0 {\n\t\treturn resp\n\t}\n\n\tusers, err := ns.userRepo.BatchGetByID(ctx, userIDs)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp\n\t}\n\tuserIDMapping := make(map[string]*entity.User, len(users))\n\tfor _, user := range users {\n\t\tuserIDMapping[user.ID] = user\n\t}\n\tfor _, item := range resp {\n\t\tif item.UserInfo == nil {\n\t\t\tcontinue\n\t\t}\n\t\tuserInfo, ok := userIDMapping[item.UserInfo.ID]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif userInfo.Status == entity.UserStatusDeleted {\n\t\t\titem.UserInfo = &schema.UserBasicInfo{\n\t\t\t\tDisplayName: \"user\" + converter.DeleteUserDisplay(userInfo.ID),\n\t\t\t\tStatus:      constant.UserDeleted,\n\t\t\t}\n\t\t}\n\t}\n\treturn resp\n}\n"
  },
  {
    "path": "internal/service/notification_common/notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage notificationcommon\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/pkg/display\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/goccy/go-json\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype NotificationRepo interface {\n\tAddNotification(ctx context.Context, notification *entity.Notification) (err error)\n\tGetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error)\n\tClearUnRead(ctx context.Context, userID string, notificationType int) (err error)\n\tClearIDUnRead(ctx context.Context, userID string, id string) (err error)\n\tGetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error)\n\tUpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error)\n\tGetById(ctx context.Context, id string) (*entity.Notification, bool, error)\n\tCountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error)\n\tDeleteNotification(ctx context.Context, userID string) (err error)\n\tDeleteUserNotificationConfig(ctx context.Context, userID string) (err error)\n}\n\ntype NotificationCommon struct {\n\tdata                     *data.Data\n\tnotificationRepo         NotificationRepo\n\tactivityRepo             activity_common.ActivityRepo\n\tfollowRepo               activity_common.FollowRepo\n\tuserCommon               *usercommon.UserCommon\n\tobjectInfoService        *object_info.ObjService\n\tnotificationQueueService noticequeue.Service\n\tuserExternalLoginRepo    user_external_login.UserExternalLoginRepo\n\tsiteInfoService          siteinfo_common.SiteInfoCommonService\n}\n\nfunc NewNotificationCommon(\n\tdata *data.Data,\n\tnotificationRepo NotificationRepo,\n\tuserCommon *usercommon.UserCommon,\n\tactivityRepo activity_common.ActivityRepo,\n\tfollowRepo activity_common.FollowRepo,\n\tobjectInfoService *object_info.ObjService,\n\tnotificationQueueService noticequeue.Service,\n\tuserExternalLoginRepo user_external_login.UserExternalLoginRepo,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n) *NotificationCommon {\n\tnotification := &NotificationCommon{\n\t\tdata:                     data,\n\t\tnotificationRepo:         notificationRepo,\n\t\tactivityRepo:             activityRepo,\n\t\tfollowRepo:               followRepo,\n\t\tuserCommon:               userCommon,\n\t\tobjectInfoService:        objectInfoService,\n\t\tnotificationQueueService: notificationQueueService,\n\t\tuserExternalLoginRepo:    userExternalLoginRepo,\n\t\tsiteInfoService:          siteInfoService,\n\t}\n\tnotificationQueueService.RegisterHandler(notification.AddNotification)\n\treturn notification\n}\n\n// AddNotification\n// need set\n// LoginUserID\n// Type  1 inbox 2 achievement\n// [inbox] Activity\n// [achievement] Rank\n// ObjectInfo.Title\n// ObjectInfo.ObjectID\n// ObjectInfo.ObjectType\nfunc (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) {\n\tif msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {\n\t\treturn nil\n\t}\n\treq := &schema.NotificationContent{\n\t\tTriggerUserID:  msg.TriggerUserID,\n\t\tReceiverUserID: msg.ReceiverUserID,\n\t\tObjectInfo: schema.ObjectInfo{\n\t\t\tTitle:      msg.Title,\n\t\t\tObjectID:   uid.DeShortID(msg.ObjectID),\n\t\t\tObjectType: msg.ObjectType,\n\t\t},\n\t\tNotificationAction: msg.NotificationAction,\n\t\tType:               msg.Type,\n\t}\n\tvar questionID string // just for notify all followers\n\tvar objInfo *schema.SimpleObjectInfo\n\tif msg.ObjectType == constant.BadgeAwardObjectType {\n\t\treq.ObjectInfo.Title = msg.Title\n\t\tobjectMap := make(map[string]string)\n\t\tobjectMap[\"badge_id\"] = msg.ExtraInfo[\"badge_id\"]\n\t\treq.ObjectInfo.ObjectMap = objectMap\n\t} else {\n\t\tobjInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t} else {\n\t\t\treq.ObjectInfo.Title = objInfo.Title\n\t\t\tquestionID = objInfo.QuestionID\n\t\t\tobjectMap := make(map[string]string)\n\t\t\tobjectMap[\"question\"] = uid.DeShortID(objInfo.QuestionID)\n\t\t\tobjectMap[\"answer\"] = uid.DeShortID(objInfo.AnswerID)\n\t\t\tobjectMap[\"comment\"] = objInfo.CommentID\n\t\t\treq.ObjectInfo.ObjectMap = objectMap\n\t\t}\n\t}\n\n\tif msg.Type == schema.NotificationTypeAchievement {\n\t\tnotificationInfo, exist, err := ns.notificationRepo.GetByUserIdObjectIdTypeId(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID, req.Type)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get by user id object id type id error: %w\", err)\n\t\t}\n\t\trank, err := ns.activityRepo.GetUserIDObjectIDActivitySum(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get user id object id activity sum error: %w\", err)\n\t\t}\n\t\treq.Rank = rank\n\t\tif exist {\n\t\t\t// modify notification\n\t\t\tupdateContent := &schema.NotificationContent{}\n\t\t\terr := json.Unmarshal([]byte(notificationInfo.Content), updateContent)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unmarshal notification content error: %w\", err)\n\t\t\t}\n\t\t\tupdateContent.Rank = rank\n\t\t\tcontent, _ := json.Marshal(updateContent)\n\t\t\tnotificationInfo.Content = string(content)\n\t\t\terr = ns.notificationRepo.UpdateNotificationContent(ctx, notificationInfo)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"update notification content error: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tinfo := &entity.Notification{}\n\tnow := time.Now()\n\tinfo.UserID = req.ReceiverUserID\n\tinfo.Type = req.Type\n\tinfo.IsRead = schema.NotificationNotRead\n\tinfo.Status = schema.NotificationStatusNormal\n\tinfo.CreatedAt = now\n\tinfo.UpdatedAt = now\n\tinfo.ObjectID = req.ObjectInfo.ObjectID\n\n\tuserBasicInfo, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, req.TriggerUserID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get user basic info error: %w\", err)\n\t}\n\tif !exist {\n\t\treturn fmt.Errorf(\"user not exist: %s\", req.TriggerUserID)\n\t}\n\treq.UserInfo = userBasicInfo\n\tcontent, _ := json.Marshal(req)\n\t_, ok := constant.NotificationMsgTypeMapping[req.NotificationAction]\n\tif ok {\n\t\tinfo.MsgType = constant.NotificationMsgTypeMapping[req.NotificationAction]\n\t}\n\tinfo.Content = string(content)\n\terr = ns.notificationRepo.AddNotification(ctx, info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"add notification error: %w\", err)\n\t}\n\terr = ns.addRedDot(ctx, info.UserID, msg.Type)\n\tif err != nil {\n\t\tlog.Error(\"addRedDot Error\", err.Error())\n\t}\n\tif req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType {\n\t\terr = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap[\"badge_id\"])\n\t\tif err != nil {\n\t\t\tlog.Error(\"AddBadgeAwardAlertCache Error\", err.Error())\n\t\t}\n\t}\n\n\tgo ns.SendNotificationToAllFollower(ctx, msg, questionID)\n\n\tif msg.Type == schema.NotificationTypeInbox {\n\t\tns.syncNotificationToPlugin(ctx, objInfo, msg)\n\t}\n\treturn nil\n}\n\nfunc (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error {\n\tvar key string\n\tif noticeType == schema.NotificationTypeInbox {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)\n\t} else {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)\n\t}\n\t_, exist, err := ns.data.Cache.GetInt64(ctx, key)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tif exist {\n\t\tif _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil {\n\t\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t\t}\n\t\treturn nil\n\t}\n\terr = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error {\n\tvar key string\n\tif notificationType == schema.NotificationTypeInbox {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)\n\t} else {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)\n\t}\n\t_, exist, err := ns.data.Cache.GetInt64(ctx, key)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tres, err := ns.data.Cache.Decrease(ctx, key, 1)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tif res <= 0 {\n\t\treturn ns.DeleteRedDot(ctx, userID, notificationType)\n\t}\n\treturn nil\n}\n\nfunc (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error {\n\tvar key string\n\tif notificationType == schema.NotificationTypeInbox {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)\n\t} else {\n\t\tkey = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)\n\t}\n\terr := ns.data.Cache.Del(ctx, key)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\n// AddBadgeAwardAlertCache add badge award alert cache\nfunc (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) {\n\tkey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)\n\tcacheData, exist, err := ns.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\tc := schema.NewRedDotBadgeAwardCache()\n\t\tc.AddBadgeAward(&schema.RedDotBadgeAward{\n\t\t\tNotificationID: notificationID,\n\t\t\tBadgeID:        badgeID,\n\t\t})\n\t\treturn ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)\n\t}\n\tc := schema.NewRedDotBadgeAwardCache()\n\tc.FromJSON(cacheData)\n\tc.AddBadgeAward(&schema.RedDotBadgeAward{\n\t\tNotificationID: notificationID,\n\t\tBadgeID:        badgeID,\n\t})\n\treturn ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)\n}\n\n// RemoveBadgeAwardAlertCache remove badge award alert cache\nfunc (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) {\n\tkey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)\n\tcacheData, exist, err := ns.data.Cache.GetString(ctx, key)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\tc := schema.NewRedDotBadgeAwardCache()\n\tc.FromJSON(cacheData)\n\tc.RemoveBadgeAward(notificationID)\n\tif len(c.BadgeAwardList) == 0 {\n\t\treturn ns.data.Cache.Del(ctx, key)\n\t}\n\treturn ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)\n}\n\n// SendNotificationToAllFollower send notification to all followers\nfunc (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg,\n\tquestionID string) {\n\tif msg.NoNeedPushAllFollow || len(questionID) == 0 {\n\t\treturn\n\t}\n\tif msg.NotificationAction != constant.NotificationUpdateQuestion &&\n\t\tmsg.NotificationAction != constant.NotificationAnswerTheQuestion &&\n\t\tmsg.NotificationAction != constant.NotificationUpdateAnswer &&\n\t\tmsg.NotificationAction != constant.NotificationAcceptAnswer {\n\t\treturn\n\t}\n\tcondObjectID := msg.ObjectID\n\tif len(questionID) > 0 {\n\t\tcondObjectID = uid.DeShortID(questionID)\n\t}\n\tuserIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tlog.Infof(\"send notification to all followers: %s %d\", condObjectID, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tt := &schema.NotificationMsg{}\n\t\t_ = copier.Copy(t, msg)\n\t\tt.ReceiverUserID = userID\n\t\tt.TriggerUserID = msg.TriggerUserID\n\t\tt.NoNeedPushAllFollow = true\n\t\tns.notificationQueueService.Send(ctx, t)\n\t}\n}\n\nfunc (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo,\n\tmsg *schema.NotificationMsg) {\n\tif objInfo == nil {\n\t\treturn\n\t}\n\tsiteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site general info failed: %v\", err)\n\t\treturn\n\t}\n\tseoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site seo info failed: %v\", err)\n\t\treturn\n\t}\n\tinterfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site interface info failed: %v\", err)\n\t\treturn\n\t}\n\n\tobjInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)\n\tobjInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)\n\tpluginNotificationMsg := plugin.NotificationMessage{\n\t\tType:           plugin.NotificationType(msg.NotificationAction),\n\t\tReceiverUserID: msg.ReceiverUserID,\n\t\tTriggerUserID:  msg.TriggerUserID,\n\t\tQuestionTitle:  objInfo.Title,\n\t}\n\n\tif len(objInfo.QuestionID) > 0 {\n\t\tpluginNotificationMsg.QuestionUrl =\n\t\t\tdisplay.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title)\n\t}\n\tif len(objInfo.AnswerID) > 0 {\n\t\tpluginNotificationMsg.AnswerUrl =\n\t\t\tdisplay.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID)\n\t}\n\tif len(objInfo.CommentID) > 0 {\n\t\tpluginNotificationMsg.CommentUrl =\n\t\t\tdisplay.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID)\n\t}\n\n\tif len(msg.TriggerUserID) > 0 {\n\t\ttriggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get trigger user basic info failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif exist {\n\t\t\tpluginNotificationMsg.TriggerUserID = triggerUser.ID\n\t\t\tpluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName\n\t\t\tpluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)\n\t\t}\n\t}\n\n\tif len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 {\n\t\tuserInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID)\n\t\tif userInfo != nil {\n\t\t\tpluginNotificationMsg.ReceiverLang = userInfo.Language\n\t\t}\n\t\t// If receiver not set language, use site default language.\n\t\tif len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption {\n\t\t\tpluginNotificationMsg.ReceiverLang = interfaceInfo.Language\n\t\t}\n\t}\n\n\texternalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, msg.ReceiverUserID)\n\tif err != nil {\n\t\tlog.Errorf(\"get user external login list failed for user %s: %v\", msg.ReceiverUserID, err)\n\t} else if len(externalLogins) > 0 {\n\t\tpluginNotificationMsg.ReceiverExternalID = externalLogins[0].ExternalID\n\t}\n\n\t_ = plugin.CallNotification(func(fn plugin.Notification) error {\n\t\tuserInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get user external login info failed: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t\tif exist {\n\t\t\tpluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID\n\t\t}\n\t\tfn.Notify(pluginNotificationMsg)\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/service/object_info/object_info.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage object_info\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// ObjService user service\ntype ObjService struct {\n\tanswerRepo   answercommon.AnswerRepo\n\tquestionRepo questioncommon.QuestionRepo\n\tcommentRepo  comment_common.CommentCommonRepo\n\ttagRepo      tagcommon.TagCommonRepo\n\ttagCommon    *tagcommon.TagCommonService\n}\n\n// NewObjService new object service\nfunc NewObjService(\n\tanswerRepo answercommon.AnswerRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\tcommentRepo comment_common.CommentCommonRepo,\n\ttagRepo tagcommon.TagCommonRepo,\n\ttagCommon *tagcommon.TagCommonService,\n) *ObjService {\n\treturn &ObjService{\n\t\tanswerRepo:   answerRepo,\n\t\tquestionRepo: questionRepo,\n\t\tcommentRepo:  commentRepo,\n\t\ttagRepo:      tagRepo,\n\t\ttagCommon:    tagCommon,\n\t}\n}\nfunc (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch objectType {\n\tcase constant.QuestionObjectType:\n\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\ttaglist, err := os.tagCommon.GetObjectEntityTag(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tos.tagCommon.TagsFormatRecommendAndReserved(ctx, taglist)\n\t\ttags, err := os.tagCommon.TagFormat(ctx, taglist)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tobjInfo = &schema.UnreviewedRevisionInfoInfo{\n\t\t\tCreatedAt:           questionInfo.CreatedAt.Unix(),\n\t\t\tObjectID:            questionInfo.ID,\n\t\t\tQuestionID:          questionInfo.ID,\n\t\t\tObjectType:          objectType,\n\t\t\tObjectCreatorUserID: questionInfo.UserID,\n\t\t\tTitle:               questionInfo.Title,\n\t\t\tContent:             questionInfo.OriginalText,\n\t\t\tHtml:                questionInfo.ParsedText,\n\t\t\tAnswerCount:         questionInfo.AnswerCount,\n\t\t\tAnswerAccepted:      !checker.IsNotZeroString(questionInfo.AcceptedAnswerID),\n\t\t\tTags:                tags,\n\t\t\tStatus:              questionInfo.Status,\n\t\t\tShowStatus:          questionInfo.Show,\n\t\t}\n\tcase constant.AnswerObjectType:\n\t\tanswerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\n\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.UnreviewedRevisionInfoInfo{\n\t\t\tCreatedAt:           answerInfo.CreatedAt.Unix(),\n\t\t\tObjectID:            answerInfo.ID,\n\t\t\tQuestionID:          answerInfo.QuestionID,\n\t\t\tAnswerID:            answerInfo.ID,\n\t\t\tObjectType:          objectType,\n\t\t\tObjectCreatorUserID: answerInfo.UserID,\n\t\t\tTitle:               questionInfo.Title,\n\t\t\tContent:             answerInfo.OriginalText,\n\t\t\tHtml:                answerInfo.ParsedText,\n\t\t\tStatus:              answerInfo.Status,\n\t\t\tAnswerAccepted:      questionInfo.AcceptedAnswerID == answerInfo.ID,\n\t\t}\n\tcase constant.TagObjectType:\n\t\ttagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.UnreviewedRevisionInfoInfo{\n\t\t\tCreatedAt:  tagInfo.CreatedAt.Unix(),\n\t\t\tObjectID:   tagInfo.ID,\n\t\t\tObjectType: objectType,\n\t\t\tTitle:      tagInfo.SlugName,\n\t\t\tContent:    tagInfo.OriginalText,\n\t\t\tHtml:       tagInfo.ParsedText,\n\t\t\tStatus:     tagInfo.Status,\n\t\t}\n\tcase constant.CommentObjectType:\n\t\tcommentInfo, exist, err := os.commentRepo.GetCommentWithoutStatus(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.UnreviewedRevisionInfoInfo{\n\t\t\tCreatedAt:           commentInfo.CreatedAt.Unix(),\n\t\t\tObjectID:            commentInfo.ID,\n\t\t\tCommentID:           commentInfo.ID,\n\t\t\tObjectType:          objectType,\n\t\t\tObjectCreatorUserID: commentInfo.UserID,\n\t\t\tContent:             commentInfo.OriginalText,\n\t\t\tHtml:                commentInfo.ParsedText,\n\t\t\tStatus:              commentInfo.Status,\n\t\t}\n\t\tif len(commentInfo.QuestionID) > 0 {\n\t\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\tobjInfo.QuestionID = questionInfo.ID\n\t\t\t}\n\t\t\tanswerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\tobjInfo.AnswerID = answerInfo.ID\n\t\t\t}\n\t\t}\n\t}\n\tif objInfo == nil {\n\t\terr = errors.BadRequest(reason.ObjectNotFound)\n\t}\n\treturn objInfo, err\n}\n\n// GetInfo get object simple information\nfunc (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *schema.SimpleObjectInfo, err error) {\n\tobjectType, err := obj.GetObjectTypeStrByObjectID(objectID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch objectType {\n\tcase constant.QuestionObjectType:\n\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.SimpleObjectInfo{\n\t\t\tObjectID:            questionInfo.ID,\n\t\t\tObjectCreatorUserID: questionInfo.UserID,\n\t\t\tQuestionID:          questionInfo.ID,\n\t\t\tQuestionStatus:      questionInfo.Status,\n\t\t\tObjectType:          objectType,\n\t\t\tTitle:               questionInfo.Title,\n\t\t\tContent:             questionInfo.ParsedText, // todo trim\n\t\t}\n\tcase constant.AnswerObjectType:\n\t\tanswerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.SimpleObjectInfo{\n\t\t\tObjectID:            answerInfo.ID,\n\t\t\tObjectCreatorUserID: answerInfo.UserID,\n\t\t\tQuestionID:          answerInfo.QuestionID,\n\t\t\tQuestionStatus:      questionInfo.Status,\n\t\t\tAnswerStatus:        answerInfo.Status,\n\t\t\tAnswerID:            answerInfo.ID,\n\t\t\tObjectType:          objectType,\n\t\t\tTitle:               questionInfo.Title,    // this should be question title\n\t\t\tContent:             answerInfo.ParsedText, // todo trim\n\t\t}\n\tcase constant.CommentObjectType:\n\t\tcommentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.SimpleObjectInfo{\n\t\t\tObjectID:            commentInfo.ID,\n\t\t\tObjectCreatorUserID: commentInfo.UserID,\n\t\t\tObjectType:          objectType,\n\t\t\tContent:             commentInfo.ParsedText, // todo trim\n\t\t\tCommentID:           commentInfo.ID,\n\t\t\tCommentStatus:       commentInfo.Status,\n\t\t}\n\t\tif len(commentInfo.QuestionID) > 0 {\n\t\t\tquestionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\tobjInfo.QuestionID = questionInfo.ID\n\t\t\t\tobjInfo.QuestionStatus = questionInfo.Status\n\t\t\t\tobjInfo.Title = questionInfo.Title\n\t\t\t}\n\t\t\tanswerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\tobjInfo.AnswerID = answerInfo.ID\n\t\t\t}\n\t\t}\n\tcase constant.TagObjectType:\n\t\ttagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\tbreak\n\t\t}\n\t\tobjInfo = &schema.SimpleObjectInfo{\n\t\t\tObjectID:            tagInfo.ID,\n\t\t\tObjectCreatorUserID: tagInfo.UserID,\n\t\t\tTagID:               tagInfo.ID,\n\t\t\tTagStatus:           tagInfo.Status,\n\t\t\tObjectType:          objectType,\n\t\t\tTitle:               tagInfo.SlugName,\n\t\t\tContent:             tagInfo.ParsedText, // todo trim\n\t\t}\n\t}\n\tif objInfo == nil {\n\t\terr = errors.BadRequest(reason.ObjectNotFound)\n\t}\n\treturn objInfo, err\n}\n"
  },
  {
    "path": "internal/service/permission/answer_permission.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage permission\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// GetAnswerPermission get answer permission\nfunc GetAnswerPermission(ctx context.Context, userID, creatorUserID string,\n\tstatus int, canEdit, canDelete, canRecover bool) (\n\tactions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif len(userID) > 0 {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"report\",\n\t\t\tName:   translator.Tr(lang, reportActionName),\n\t\t\tType:   \"reason\",\n\t\t})\n\t}\n\tif canEdit || userID == creatorUserID {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"edit\",\n\t\t\tName:   translator.Tr(lang, editActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\n\tif (canDelete || userID == creatorUserID) && status != entity.AnswerStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"delete\",\n\t\t\tName:   translator.Tr(lang, deleteActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\n\tif canRecover && status == entity.AnswerStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"undelete\",\n\t\t\tName:   translator.Tr(lang, undeleteActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\treturn actions\n}\n"
  },
  {
    "path": "internal/service/permission/comment_permission.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage permission\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// GetCommentPermission get comment permission\nfunc GetCommentPermission(ctx context.Context, userID string, creatorUserID string,\n\tcreatedAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif len(userID) > 0 {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"report\",\n\t\t\tName:   translator.Tr(lang, reportActionName),\n\t\t\tType:   \"reason\",\n\t\t})\n\t}\n\tdeadline := createdAt.Add(constant.CommentEditDeadline)\n\tif canEdit || (userID == creatorUserID && time.Now().Before(deadline)) {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"edit\",\n\t\t\tName:   translator.Tr(lang, editActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\n\tif canDelete || userID == creatorUserID {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"delete\",\n\t\t\tName:   translator.Tr(lang, deleteActionName),\n\t\t\tType:   \"reason\",\n\t\t})\n\t}\n\treturn actions\n}\n"
  },
  {
    "path": "internal/service/permission/permission_name.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage permission\n\nconst (\n\tAdminAccess                 = \"admin.access\"\n\tQuestionAdd                 = \"question.add\"\n\tQuestionEdit                = \"question.edit\"\n\tQuestionEditWithoutReview   = \"question.edit_without_review\"\n\tQuestionDelete              = \"question.delete\"\n\tQuestionClose               = \"question.close\"\n\tQuestionReopen              = \"question.reopen\"\n\tQuestionVoteUp              = \"question.vote_up\"\n\tQuestionVoteDown            = \"question.vote_down\"\n\tQuestionPin                 = \"question.pin\"\n\tQuestionUnPin               = \"question.unpin\"\n\tQuestionHide                = \"question.hide\"\n\tQuestionShow                = \"question.show\"\n\tAnswerAdd                   = \"answer.add\"\n\tAnswerEdit                  = \"answer.edit\"\n\tAnswerEditWithoutReview     = \"answer.edit_without_review\"\n\tAnswerDelete                = \"answer.delete\"\n\tAnswerAccept                = \"answer.accept\"\n\tAnswerVoteUp                = \"answer.vote_up\"\n\tAnswerVoteDown              = \"answer.vote_down\"\n\tAnswerInviteSomeoneToAnswer = \"answer.invite_someone_to_answer\"\n\tCommentAdd                  = \"comment.add\"\n\tCommentEdit                 = \"comment.edit\"\n\tCommentDelete               = \"comment.delete\"\n\tCommentVoteUp               = \"comment.vote_up\"\n\tCommentVoteDown             = \"comment.vote_down\"\n\tReportAdd                   = \"report.add\"\n\tTagAdd                      = \"tag.add\"\n\tTagEdit                     = \"tag.edit\"\n\tTagEditSlugName             = \"tag.edit_slug_name\"\n\tTagEditWithoutReview        = \"tag.edit_without_review\"\n\tTagDelete                   = \"tag.delete\"\n\tTagMerge                    = \"tag.merge\"\n\tTagSynonym                  = \"tag.synonym\"\n\tLinkUrlLimit                = \"link.url_limit\"\n\tVoteDetail                  = \"vote.detail\"\n\tAnswerAudit                 = \"answer.audit\"\n\tQuestionAudit               = \"question.audit\"\n\tTagAudit                    = \"tag.audit\"\n\tTagUseReservedTag           = \"tag.use_reserved_tag\"\n\tAnswerUnDelete              = \"answer.undeleted\"\n\tQuestionUnDelete            = \"question.undeleted\"\n\tTagUnDelete                 = \"tag.undeleted\"\n)\n\nconst (\n\treportActionName                = \"action.report\"\n\teditActionName                  = \"action.edit\"\n\tdeleteActionName                = \"action.delete\"\n\tmergeActionName                 = \"action.merge\"\n\tundeleteActionName              = \"action.undelete\"\n\tcloseActionName                 = \"action.close\"\n\treopenActionName                = \"action.reopen\"\n\tpinActionName                   = \"action.pin\"\n\tunpinActionName                 = \"action.unpin\"\n\thideActionName                  = \"action.hide\"\n\tshowActionName                  = \"action.show\"\n\tinviteSomeoneToAnswerActionName = \"action.invite_someone_to_answer\"\n)\n"
  },
  {
    "path": "internal/service/permission/question_permission.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage permission\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// GetQuestionPermission get question permission\nfunc GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, status int,\n\tcanEdit, canDelete, canClose, canReopen, canPin, canHide, canUnPin, canShow, canRecover bool) (\n\tactions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif len(userID) > 0 {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"report\",\n\t\t\tName:   translator.Tr(lang, reportActionName),\n\t\t\tType:   \"reason\",\n\t\t})\n\t}\n\tif (canEdit || userID == creatorUserID) && status != entity.QuestionStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"edit\",\n\t\t\tName:   translator.Tr(lang, editActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\tif canClose && status == entity.QuestionStatusAvailable {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"close\",\n\t\t\tName:   translator.Tr(lang, closeActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\tif canReopen {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"reopen\",\n\t\t\tName:   translator.Tr(lang, reopenActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\tif canPin {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"pin\",\n\t\t\tName:   translator.Tr(lang, pinActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\tif canHide {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"hide\",\n\t\t\tName:   translator.Tr(lang, hideActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\n\tif canUnPin {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"unpin\",\n\t\t\tName:   translator.Tr(lang, unpinActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\n\tif canShow {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"show\",\n\t\t\tName:   translator.Tr(lang, showActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\n\tif (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"delete\",\n\t\t\tName:   translator.Tr(lang, deleteActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\n\tif canRecover && status == entity.QuestionStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"undelete\",\n\t\t\tName:   translator.Tr(lang, undeleteActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\treturn actions\n}\n\n// GetQuestionExtendsPermission get question extends permission\nfunc GetQuestionExtendsPermission(ctx context.Context, canInviteOtherToAnswer bool) (\n\tactions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif canInviteOtherToAnswer {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"invite_other_to_answer\",\n\t\t\tName:   translator.Tr(lang, inviteSomeoneToAnswerActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\treturn actions\n}\n"
  },
  {
    "path": "internal/service/permission/tag_permission.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage permission\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// GetTagPermission get tag permission\nfunc GetTagPermission(ctx context.Context, status int, canEdit, canDelete, canMerge, canRecover bool) (\n\tactions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif canEdit {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"edit\",\n\t\t\tName:   translator.Tr(lang, editActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\n\tif canDelete && status != entity.TagStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"delete\",\n\t\t\tName:   translator.Tr(lang, deleteActionName),\n\t\t\tType:   \"reason\",\n\t\t})\n\t}\n\n\tif canMerge && status != entity.TagStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"merge\",\n\t\t\tName:   translator.Tr(lang, mergeActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\n\tif canRecover && status == entity.QuestionStatusDeleted {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"undelete\",\n\t\t\tName:   translator.Tr(lang, undeleteActionName),\n\t\t\tType:   \"confirm\",\n\t\t})\n\t}\n\treturn actions\n}\n\n// GetTagSynonymPermission get tag synonym permission\nfunc GetTagSynonymPermission(ctx context.Context, canEdit bool) (\n\tactions []*schema.PermissionMemberAction) {\n\tlang := handler.GetLangByCtx(ctx)\n\tactions = make([]*schema.PermissionMemberAction, 0)\n\tif canEdit {\n\t\tactions = append(actions, &schema.PermissionMemberAction{\n\t\t\tAction: \"edit\",\n\t\t\tName:   translator.Tr(lang, editActionName),\n\t\t\tType:   \"edit\",\n\t\t})\n\t}\n\treturn actions\n}\n"
  },
  {
    "path": "internal/service/plugin_common/plugin_common_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin_common\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/repo/search_sync\"\n\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/importer\"\n\t\"github.com/apache/answer/plugin\"\n)\n\ntype PluginConfigRepo interface {\n\tSavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error)\n\tGetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error)\n}\n\ntype PluginUserConfigRepo interface {\n\tSaveUserPluginConfig(ctx context.Context, userID string, pluginSlugName, configValue string) (err error)\n\tGetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) (\n\t\tpluginUserConfig *entity.PluginUserConfig, exist bool, err error)\n\tGetPluginUserConfigPage(ctx context.Context, page, pageSize int) (\n\t\tpluginUserConfigs []*entity.PluginUserConfig, total int64, err error)\n\tDeleteUserPluginConfig(ctx context.Context, userID string) (err error)\n}\n\n// PluginCommonService user service\ntype PluginCommonService struct {\n\tconfigService        *config.ConfigService\n\tpluginConfigRepo     PluginConfigRepo\n\tpluginUserConfigRepo PluginUserConfigRepo\n\tdata                 *data.Data\n\timporterService      *importer.ImporterService\n}\n\n// NewPluginCommonService new report service\nfunc NewPluginCommonService(\n\tpluginConfigRepo PluginConfigRepo,\n\tpluginUserConfigRepo PluginUserConfigRepo,\n\tconfigService *config.ConfigService,\n\tdata *data.Data,\n\timporterService *importer.ImporterService,\n) *PluginCommonService {\n\tp := &PluginCommonService{\n\t\tconfigService:        configService,\n\t\tpluginConfigRepo:     pluginConfigRepo,\n\t\tpluginUserConfigRepo: pluginUserConfigRepo,\n\t\tdata:                 data,\n\t\timporterService:      importerService,\n\t}\n\tp.initPluginData()\n\treturn p\n}\n\n// UpdatePluginStatus update plugin status\nfunc (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) {\n\tcontent, err := plugin.StatusManager.MarshalJSON()\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err)\n\t}\n\treturn ps.configService.UpdateConfig(ctx, constant.PluginStatus, string(content))\n}\n\n// UpdatePluginConfig update plugin config\nfunc (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) {\n\tconfigValue, _ := json.Marshal(req.ConfigFields)\n\terr = ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_ = plugin.CallSearch(func(search plugin.Search) error {\n\t\tif search.Info().SlugName == req.PluginSlugName {\n\t\t\tsearch.RegisterSyncer(ctx, search_sync.NewPluginSyncer(ps.data))\n\t\t}\n\t\treturn nil\n\t})\n\t_ = plugin.CallImporter(func(importer plugin.Importer) error {\n\t\timporter.RegisterImporterFunc(ctx, ps.importerService.NewImporterFunc())\n\t\treturn nil\n\t})\n\treturn nil\n}\n\n// UpdatePluginUserConfig update plugin config\nfunc (ps *PluginCommonService) UpdatePluginUserConfig(ctx context.Context, req *schema.UpdateUserPluginConfigReq) (err error) {\n\tconfigValue, _ := json.Marshal(req.ConfigFields)\n\terr = ps.pluginUserConfigRepo.SaveUserPluginConfig(ctx, req.UserID, req.PluginSlugName, string(configValue))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetUserPluginConfig get user plugin config\nfunc (ps *PluginCommonService) GetUserPluginConfig(ctx context.Context, req *schema.GetUserPluginConfigReq) (\n\tconfigValue string, err error) {\n\tpluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(ctx, req.UserID, req.PluginSlugName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !exist {\n\t\treturn \"\", nil\n\t}\n\treturn pluginUserConfig.Value, nil\n}\n\nfunc (ps *PluginCommonService) initPluginData() {\n\t_ = plugin.CallKVStorage(func(k plugin.KVStorage) error {\n\t\tk.SetOperator(plugin.NewKVOperator(\n\t\t\tps.data.DB,\n\t\t\tps.data.Cache,\n\t\t\tk.Info().SlugName,\n\t\t))\n\t\treturn nil\n\t})\n\n\t// init plugin status\n\tpluginStatus, err := ps.configService.GetStringValueFromDB(context.TODO(), constant.PluginStatus)\n\tif err != nil {\n\t\tlog.Error(err)\n\t} else {\n\t\tif err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\n\t// init plugin config\n\tpluginConfigs, err := ps.pluginConfigRepo.GetPluginConfigAll(context.Background())\n\tif err != nil {\n\t\tlog.Error(err)\n\t} else {\n\t\tfor _, pluginConfig := range pluginConfigs {\n\t\t\terr := plugin.CallConfig(func(fn plugin.Config) error {\n\t\t\t\tif fn.Info().SlugName == pluginConfig.PluginSlugName {\n\t\t\t\t\treturn fn.ConfigReceiver([]byte(pluginConfig.Value))\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"parse plugin config failed: %s %v\", pluginConfig.PluginSlugName, err)\n\t\t\t}\n\t\t}\n\n\t\t_ = plugin.CallCache(func(cache plugin.Cache) error {\n\t\t\tps.data.Cache = cache\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// init plugin user config\n\tplugin.RegisterGetPluginUserConfigFunc(func(userID, pluginSlugName string) []byte {\n\t\tpluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(context.Background(), userID, pluginSlugName)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn nil\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil\n\t\t}\n\t\treturn []byte(pluginUserConfig.Value)\n\t})\n\n\t// init plugin user config data\n\tgo func() {\n\t\tpage, pageSize := 1, 1000\n\t\tfor {\n\t\t\tuserConfigs, _, err := ps.pluginUserConfigRepo.GetPluginUserConfigPage(context.Background(), page, pageSize)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif len(userConfigs) == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor _, userConfig := range userConfigs {\n\t\t\t\terr := plugin.CallUserConfig(func(fn plugin.UserConfig) error {\n\t\t\t\t\tif fn.Info().SlugName == userConfig.PluginSlugName {\n\t\t\t\t\t\treturn fn.UserConfigReceiver(userConfig.UserID, []byte(userConfig.Value))\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"parse plugin user config failed: %s %v\", userConfig.PluginSlugName, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tpage++\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/service/provider.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage service\n\nimport (\n\t\"github.com/apache/answer/internal/service/action\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/internal/service/ai_conversation\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/apikey\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/internal/service/collection\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/internal/service/dashboard\"\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/feature_toggle\"\n\t\"github.com/apache/answer/internal/service/file_record\"\n\t\"github.com/apache/answer/internal/service/follow\"\n\t\"github.com/apache/answer/internal/service/importer\"\n\t\"github.com/apache/answer/internal/service/meta\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/notification\"\n\tnotficationcommon \"github.com/apache/answer/internal/service/notification_common\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/rank\"\n\t\"github.com/apache/answer/internal/service/reason\"\n\t\"github.com/apache/answer/internal/service/report\"\n\t\"github.com/apache/answer/internal/service/report_handle\"\n\t\"github.com/apache/answer/internal/service/review\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/search_parser\"\n\t\"github.com/apache/answer/internal/service/siteinfo\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/internal/service/tag\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/internal/service/uploader\"\n\t\"github.com/apache/answer/internal/service/user_admin\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/google/wire\"\n)\n\n// ProviderSetService is providers.\nvar ProviderSetService = wire.NewSet(\n\tcomment.NewCommentService,\n\tcomment_common.NewCommentCommonService,\n\treport.NewReportService,\n\tcontent.NewVoteService,\n\ttag.NewTagService,\n\tfollow.NewFollowService,\n\tcollection.NewCollectionGroupService,\n\tcollection.NewCollectionService,\n\taction.NewCaptchaService,\n\tauth.NewAuthService,\n\tcontent.NewUserService,\n\tcontent.NewQuestionService,\n\tcontent.NewAnswerService,\n\texport.NewEmailService,\n\ttagcommon.NewTagCommonService,\n\tusercommon.NewUserCommon,\n\tquestioncommon.NewQuestionCommon,\n\tanswercommon.NewAnswerCommon,\n\tuploader.NewUploaderService,\n\tcollectioncommon.NewCollectionCommon,\n\trevision_common.NewRevisionService,\n\tcontent.NewRevisionService,\n\trank.NewRankService,\n\tsearch_parser.NewSearchParser,\n\tcontent.NewSearchService,\n\tmetacommon.NewMetaCommonService,\n\tobject_info.NewObjService,\n\treport_handle.NewReportHandle,\n\tuser_admin.NewUserAdminService,\n\treason.NewReasonService,\n\tsiteinfo_common.NewSiteInfoCommonService,\n\tsiteinfo.NewSiteInfoService,\n\tnotficationcommon.NewNotificationCommon,\n\tnotification.NewNotificationService,\n\tactivity.NewAnswerActivityService,\n\tdashboard.NewDashboardService,\n\tactivity_common.NewActivityCommon,\n\tactivity.NewActivityService,\n\trole.NewRoleService,\n\trole.NewUserRoleRelService,\n\trole.NewRolePowerRelService,\n\tuser_external_login.NewUserExternalLoginService,\n\tuser_external_login.NewUserCenterLoginService,\n\tplugin_common.NewPluginCommonService,\n\tconfig.NewConfigService,\n\tnoticequeue.NewService,\n\tactivityqueue.NewService,\n\tuser_notification_config.NewUserNotificationConfigService,\n\tnotification.NewExternalNotificationService,\n\tnoticequeue.NewExternalService,\n\treview.NewReviewService,\n\tmeta.NewMetaService,\n\teventqueue.NewService,\n\tbadge.NewBadgeService,\n\tbadge.NewBadgeEventService,\n\tbadge.NewBadgeAwardService,\n\tbadge.NewBadgeGroupService,\n\timporter.NewImporterService,\n\tfile_record.NewFileRecordService,\n\tapikey.NewAPIKeyService,\n\tai_conversation.NewAIConversationService,\n\tfeature_toggle.NewFeatureToggleService,\n)\n"
  },
  {
    "path": "internal/service/question_common/question.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage questioncommon\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/internal/service/config\"\n\tmetacommon \"github.com/apache/answer/internal/service/meta_common\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tcollectioncommon \"github.com/apache/answer/internal/service/collection_common\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// QuestionRepo question repository\ntype QuestionRepo interface {\n\tAddQuestion(ctx context.Context, question *entity.Question) (err error)\n\tRemoveQuestion(ctx context.Context, id string) (err error)\n\tUpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)\n\tGetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)\n\tGetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)\n\tGetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) (\n\t\tquestionList []*entity.Question, total int64, err error)\n\tGetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error)\n\tUpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error)\n\tUpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)\n\tDeletePermanentlyQuestions(ctx context.Context) (err error)\n\tRecoverQuestion(ctx context.Context, questionID string) (err error)\n\tUpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error)\n\tGetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error)\n\tUpdatePvCount(ctx context.Context, questionID string) (err error)\n\tUpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)\n\tUpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error)\n\tUpdateAccepted(ctx context.Context, question *entity.Question) (err error)\n\tUpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)\n\tFindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)\n\tAdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error)\n\tGetQuestionCount(ctx context.Context) (count int64, err error)\n\tGetUnansweredQuestionCount(ctx context.Context) (count int64, err error)\n\tGetResolvedQuestionCount(ctx context.Context) (count int64, err error)\n\tGetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error)\n\tSitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)\n\tRemoveAllUserQuestion(ctx context.Context, userID string) (err error)\n\tUpdateSearch(ctx context.Context, questionID string) (err error)\n\tLinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error)\n\tGetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error)\n\tUpdateQuestionLinkCount(ctx context.Context, questionID string) (err error)\n\tRemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)\n\tRecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)\n\tUpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error)\n\tGetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questions []*entity.Question, total int64, err error)\n}\n\n// QuestionCommon user service\ntype QuestionCommon struct {\n\tquestionRepo         QuestionRepo\n\tanswerRepo           answercommon.AnswerRepo\n\tvoteRepo             activity_common.VoteRepo\n\tfollowCommon         activity_common.FollowRepo\n\ttagCommon            *tagcommon.TagCommonService\n\tuserCommon           *usercommon.UserCommon\n\tcollectionCommon     *collectioncommon.CollectionCommon\n\tAnswerCommon         *answercommon.AnswerCommon\n\tmetaCommonService    *metacommon.MetaCommonService\n\tconfigService        *config.ConfigService\n\tactivityQueueService activityqueue.Service\n\trevisionRepo         revision.RevisionRepo\n\tsiteInfoService      siteinfo_common.SiteInfoCommonService\n\tdata                 *data.Data\n}\n\nfunc NewQuestionCommon(questionRepo QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\tvoteRepo activity_common.VoteRepo,\n\tfollowCommon activity_common.FollowRepo,\n\ttagCommon *tagcommon.TagCommonService,\n\tuserCommon *usercommon.UserCommon,\n\tcollectionCommon *collectioncommon.CollectionCommon,\n\tanswerCommon *answercommon.AnswerCommon,\n\tmetaCommonService *metacommon.MetaCommonService,\n\tconfigService *config.ConfigService,\n\tactivityQueueService activityqueue.Service,\n\trevisionRepo revision.RevisionRepo,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tdata *data.Data,\n) *QuestionCommon {\n\treturn &QuestionCommon{\n\t\tquestionRepo:         questionRepo,\n\t\tanswerRepo:           answerRepo,\n\t\tvoteRepo:             voteRepo,\n\t\tfollowCommon:         followCommon,\n\t\ttagCommon:            tagCommon,\n\t\tuserCommon:           userCommon,\n\t\tcollectionCommon:     collectionCommon,\n\t\tAnswerCommon:         answerCommon,\n\t\tmetaCommonService:    metaCommonService,\n\t\tconfigService:        configService,\n\t\tactivityQueueService: activityQueueService,\n\t\trevisionRepo:         revisionRepo,\n\t\tsiteInfoService:      siteInfoService,\n\t\tdata:                 data,\n\t}\n}\n\nfunc (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {\n\treturn qs.questionRepo.GetUserQuestionCount(ctx, userID, 0)\n}\n\nfunc (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) {\n\tshow := entity.QuestionShow\n\tif loginUserID == userID || isAdmin {\n\t\tshow = 0\n\t}\n\treturn qs.questionRepo.GetUserQuestionCount(ctx, userID, show)\n}\n\nfunc (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {\n\treturn qs.questionRepo.UpdatePvCount(ctx, questionID)\n}\n\nfunc (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string) error {\n\tcount, err := qs.answerRepo.GetCountByQuestionID(ctx, questionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif count == 0 {\n\t\terr = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{\n\t\t\tID:           questionID,\n\t\t\tLastAnswerID: \"0\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count))\n}\n\nfunc (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) {\n\treturn qs.questionRepo.UpdateCollectionCount(ctx, questionID)\n}\n\nfunc (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, answerID string) error {\n\tquestion := &entity.Question{}\n\tquestion.ID = questionID\n\tquestion.AcceptedAnswerID = answerID\n\treturn qs.questionRepo.UpdateAccepted(ctx, question)\n}\n\nfunc (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, answerID string) error {\n\tquestion := &entity.Question{}\n\tquestion.ID = questionID\n\tquestion.LastAnswerID = answerID\n\treturn qs.questionRepo.UpdateLastAnswer(ctx, question)\n}\n\nfunc (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error {\n\tquestioninfo := &entity.Question{}\n\tnow := time.Now()\n\tquestioninfo.ID = questionID\n\tquestioninfo.PostUpdateTime = now\n\treturn qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{\"post_update_time\"})\n}\nfunc (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error {\n\tquestioninfo := &entity.Question{}\n\tquestioninfo.ID = questionID\n\tquestioninfo.PostUpdateTime = setTime\n\treturn qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{\"post_update_time\"})\n}\n\nfunc (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) {\n\tlist := make(map[string]*schema.QuestionInfoResp)\n\tquestionList, err := qs.questionRepo.FindByID(ctx, questionIDs)\n\tif err != nil {\n\t\treturn list, err\n\t}\n\tquestions, err := qs.FormatQuestions(ctx, questionList, loginUserID)\n\tif err != nil {\n\t\treturn list, err\n\t}\n\tfor _, item := range questions {\n\t\tlist[item.ID] = item\n\t}\n\treturn list, nil\n}\n\nfunc (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {\n\tInviteUserInfo := make([]*schema.UserBasicInfo, 0)\n\tdbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)\n\tif err != nil {\n\t\treturn InviteUserInfo, err\n\t}\n\tif !has {\n\t\treturn InviteUserInfo, errors.NotFound(reason.QuestionNotFound)\n\t}\n\t// InviteUser\n\tif dbinfo.InviteUserID != \"\" {\n\t\tInviteUserIDs := make([]string, 0)\n\t\terr := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs)\n\t\tif err == nil {\n\t\t\tinviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs)\n\t\t\tif err == nil {\n\t\t\t\tfor _, userid := range InviteUserIDs {\n\t\t\t\t\t_, ok := inviteUserInfoMap[userid]\n\t\t\t\t\tif ok {\n\t\t\t\t\t\tInviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn InviteUserInfo, nil\n}\n\nfunc (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tquestionInfo.ID = uid.DeShortID(questionInfo.ID)\n\tif !has {\n\t\treturn resp, errors.NotFound(reason.QuestionNotFound)\n\t}\n\tresp = qs.ShowFormat(ctx, questionInfo)\n\tif resp.Status == entity.QuestionStatusClosed {\n\t\tmetaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\tcloseMsg := &schema.CloseQuestionMeta{}\n\t\t\terr = json.Unmarshal([]byte(metaInfo.Value), closeMsg)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"json.Unmarshal CloseQuestionMeta error\", err.Error())\n\t\t\t} else {\n\t\t\t\tcfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Error(\"json.Unmarshal QuestionCloseJson error\", err.Error())\n\t\t\t\t} else {\n\t\t\t\t\treasonItem := &schema.ReasonItem{}\n\t\t\t\t\t_ = json.Unmarshal(cfg.GetByteValue(), reasonItem)\n\t\t\t\t\treasonItem.Translate(cfg.Key, handler.GetLangByCtx(ctx))\n\t\t\t\t\toperation := &schema.Operation{}\n\t\t\t\t\toperation.Type = reasonItem.Name\n\t\t\t\t\toperation.Description = reasonItem.Description\n\t\t\t\t\toperation.Msg = closeMsg.CloseMsg\n\t\t\t\t\toperation.Time = metaInfo.CreatedAt.Unix()\n\t\t\t\t\toperation.Level = schema.OperationLevelInfo\n\t\t\t\t\tresp.Operation = operation\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif resp.Status != entity.QuestionStatusDeleted {\n\t\tif resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil {\n\t\t\treturn resp, err\n\t\t}\n\t} else {\n\t\trevisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get revision error %s\", err)\n\t\t}\n\t\tif exist {\n\t\t\tquestionWithTagsRevision := &entity.QuestionWithTagsRevision{}\n\t\t\tif err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil {\n\t\t\t\tlog.Errorf(\"revision parsing error %s\", err)\n\t\t\t\treturn resp, nil\n\t\t\t}\n\t\t\tfor _, tag := range questionWithTagsRevision.Tags {\n\t\t\t\tresp.Tags = append(resp.Tags, &schema.TagResp{\n\t\t\t\t\tID:              tag.ID,\n\t\t\t\t\tSlugName:        tag.SlugName,\n\t\t\t\t\tDisplayName:     tag.DisplayName,\n\t\t\t\t\tMainTagSlugName: tag.MainTagSlugName,\n\t\t\t\t\tRecommend:       tag.Recommend,\n\t\t\t\t\tReserved:        tag.Reserved,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tuserIds := make([]string, 0)\n\tif checker.IsNotZeroString(questionInfo.UserID) {\n\t\tuserIds = append(userIds, questionInfo.UserID)\n\t}\n\tif checker.IsNotZeroString(questionInfo.LastEditUserID) {\n\t\tuserIds = append(userIds, questionInfo.LastEditUserID)\n\t}\n\tif checker.IsNotZeroString(resp.LastAnsweredUserID) {\n\t\tuserIds = append(userIds, resp.LastAnsweredUserID)\n\t}\n\tuserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tresp.UserInfo = userInfoMap[questionInfo.UserID]\n\tresp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID]\n\tresp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID]\n\tif len(loginUserID) == 0 {\n\t\treturn resp, nil\n\t}\n\n\tresp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID)\n\tresp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID)\n\n\tids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID)\n\tif err != nil {\n\t\tlog.Error(\"AnswerFunc.SearchAnswerIDs\", err)\n\t}\n\tresp.Answered = len(ids) > 0\n\tif resp.Answered {\n\t\tresp.FirstAnswerId = ids[0]\n\t}\n\n\tcollectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(collectedMap) > 0 {\n\t\tresp.Collected = true\n\t}\n\treturn resp, nil\n}\n\nfunc (qs *QuestionCommon) FormatQuestionsPage(\n\tctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (\n\tformattedQuestions []*schema.QuestionPageResp, err error) {\n\tformattedQuestions = make([]*schema.QuestionPageResp, 0)\n\tquestionIDs := make([]string, 0)\n\tuserIDs := make([]string, 0)\n\tfor _, questionInfo := range questionList {\n\t\tt := &schema.QuestionPageResp{\n\t\t\tID:               questionInfo.ID,\n\t\t\tCreatedAt:        questionInfo.CreatedAt.Unix(),\n\t\t\tTitle:            questionInfo.Title,\n\t\t\tUrlTitle:         htmltext.UrlTitle(questionInfo.Title),\n\t\t\tDescription:      htmltext.FetchExcerpt(questionInfo.ParsedText, \"...\", 240),\n\t\t\tStatus:           questionInfo.Status,\n\t\t\tViewCount:        questionInfo.ViewCount,\n\t\t\tUniqueViewCount:  questionInfo.UniqueViewCount,\n\t\t\tVoteCount:        questionInfo.VoteCount,\n\t\t\tAnswerCount:      questionInfo.AnswerCount,\n\t\t\tCollectionCount:  questionInfo.CollectionCount,\n\t\t\tFollowCount:      questionInfo.FollowCount,\n\t\t\tAcceptedAnswerID: questionInfo.AcceptedAnswerID,\n\t\t\tLastAnswerID:     questionInfo.LastAnswerID,\n\t\t\tPin:              questionInfo.Pin,\n\t\t\tShow:             questionInfo.Show,\n\t\t\tOperator:         &schema.QuestionPageRespOperator{ID: questionInfo.UserID},\n\t\t}\n\n\t\tquestionIDs = append(questionIDs, questionInfo.ID)\n\t\tuserIDs = append(userIDs, questionInfo.UserID)\n\t\thaveEdited, haveAnswered := false, false\n\t\tif checker.IsNotZeroString(questionInfo.LastEditUserID) {\n\t\t\thaveEdited = true\n\t\t\tuserIDs = append(userIDs, questionInfo.LastEditUserID)\n\t\t}\n\t\tif checker.IsNotZeroString(questionInfo.LastAnswerID) {\n\t\t\thaveAnswered = true\n\n\t\t\tanswerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID)\n\t\t\tif err == nil && exist {\n\t\t\t\tif answerInfo.LastEditUserID != \"0\" {\n\t\t\t\t\tt.LastAnsweredUserID = answerInfo.LastEditUserID\n\t\t\t\t} else {\n\t\t\t\t\tt.LastAnsweredUserID = answerInfo.UserID\n\t\t\t\t}\n\t\t\t\tt.LastAnsweredAt = answerInfo.CreatedAt\n\t\t\t\tuserIDs = append(userIDs, t.LastAnsweredUserID)\n\t\t\t}\n\t\t}\n\n\t\t// The default operation is to ask questions\n\t\tt.OperationType = schema.QuestionPageRespOperationTypeAsked\n\t\tt.OperatedAt = questionInfo.CreatedAt.Unix()\n\t\tt.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}\n\n\t\t// If the order is active, the last operation time is the last edit or answer time if it exists\n\t\tif orderCond == schema.QuestionOrderCondActive {\n\t\t\tif haveEdited {\n\t\t\t\tt.OperationType = schema.QuestionPageRespOperationTypeModified\n\t\t\t\tt.OperatedAt = questionInfo.UpdatedAt.Unix()\n\t\t\t\tt.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}\n\t\t\t}\n\t\t\tif haveAnswered {\n\t\t\t\tif t.LastAnsweredAt.Unix() > t.OperatedAt {\n\t\t\t\t\tt.OperationType = schema.QuestionPageRespOperationTypeAnswered\n\t\t\t\t\tt.OperatedAt = t.LastAnsweredAt.Unix()\n\t\t\t\t\tt.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tformattedQuestions = append(formattedQuestions, t)\n\t}\n\n\ttagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs)\n\tif err != nil {\n\t\treturn formattedQuestions, err\n\t}\n\tuserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs)\n\tif err != nil {\n\t\treturn formattedQuestions, err\n\t}\n\n\tfor _, item := range formattedQuestions {\n\t\ttags, ok := tagsMap[item.ID]\n\t\tif ok {\n\t\t\titem.Tags = tags\n\t\t} else {\n\t\t\titem.Tags = make([]*schema.TagResp, 0)\n\t\t}\n\t\tuserInfo, ok := userInfoMap[item.Operator.ID]\n\t\tif ok {\n\t\t\tif userInfo != nil {\n\t\t\t\titem.Operator.DisplayName = userInfo.DisplayName\n\t\t\t\titem.Operator.Username = userInfo.Username\n\t\t\t\titem.Operator.Rank = userInfo.Rank\n\t\t\t\titem.Operator.Status = userInfo.Status\n\t\t\t\titem.Operator.Avatar = userInfo.Avatar\n\t\t\t}\n\t\t}\n\t}\n\treturn formattedQuestions, nil\n}\n\nfunc (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) {\n\tlist := make([]*schema.QuestionInfoResp, 0)\n\tobjectIds := make([]string, 0)\n\tuserIds := make([]string, 0)\n\n\tfor _, questionInfo := range questionList {\n\t\titem := qs.ShowFormat(ctx, questionInfo)\n\t\tlist = append(list, item)\n\t\tobjectIds = append(objectIds, item.ID)\n\t\tuserIds = append(userIds, item.UserID, item.LastEditUserID, item.LastAnsweredUserID)\n\t}\n\ttagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds)\n\tif err != nil {\n\t\treturn list, err\n\t}\n\n\tuserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)\n\tif err != nil {\n\t\treturn list, err\n\t}\n\n\tfor _, item := range list {\n\t\titem.Tags = tagsMap[item.ID]\n\t\titem.UserInfo = userInfoMap[item.UserID]\n\t\titem.UpdateUserInfo = userInfoMap[item.LastEditUserID]\n\t\titem.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID]\n\t}\n\tif loginUserID == \"\" {\n\t\treturn list, nil\n\t}\n\n\tcollectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, item := range list {\n\t\titem.Collected = collectedMap[item.ID]\n\t}\n\treturn list, nil\n}\n\n// RemoveQuestion delete question\nfunc (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\n\tif questionInfo.Status == entity.QuestionStatusDeleted {\n\t\treturn nil\n\t}\n\n\tquestionInfo.Status = entity.QuestionStatusDeleted\n\terr = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID)\n\tif err != nil {\n\t\tlog.Error(\"user GetUserQuestionCount error\", err.Error())\n\t} else {\n\t\terr = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)\n\t\tif err != nil {\n\t\t\tlog.Error(\"user IncreaseQuestionCount error\", err.Error())\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error {\n\tquestionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\tquestionInfo.Status = entity.QuestionStatusClosed\n\terr = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcloseMeta, _ := json.Marshal(schema.CloseQuestionMeta{\n\t\tCloseType: req.CloseType,\n\t\tCloseMsg:  req.CloseMsg,\n\t})\n\terr = qs.metaCommonService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tqs.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           questionInfo.UserID,\n\t\tObjectID:         questionInfo.ID,\n\t\tOriginalObjectID: questionInfo.ID,\n\t\tActivityTypeKey:  constant.ActQuestionClosed,\n\t})\n\treturn nil\n}\n\n// RemoveAnswer delete answer\nfunc (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) {\n\tanswerinfo, has, err := qs.answerRepo.GetByID(ctx, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !has {\n\t\treturn nil\n\t}\n\n\t// user add question count\n\n\terr = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID)\n\tif err != nil {\n\t\tlog.Error(\"UpdateAnswerCount error\", err.Error())\n\t}\n\tuserAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID)\n\tif err != nil {\n\t\tlog.Error(\"GetCountByUserID error\", err.Error())\n\t}\n\terr = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount))\n\tif err != nil {\n\t\tlog.Error(\"user UpdateAnswerCount error\", err.Error())\n\t}\n\n\treturn qs.answerRepo.RemoveAnswer(ctx, id)\n}\n\nfunc (qs *QuestionCommon) SitemapCron(ctx context.Context) {\n\tquestionNum, err := qs.questionRepo.GetQuestionCount(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif questionNum <= constant.SitemapMaxSize {\n\t\t_, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum))\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site map question error: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\n\ttotalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))\n\tfor i := 1; i <= totalPages; i++ {\n\t\t_, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get site map question error: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info any) error {\n\tinfoStr, err := json.Marshal(info)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\terr = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime)\n\tif err != nil {\n\t\treturn errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\treturn nil\n}\n\nfunc (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {\n\treturn qs.ShowFormat(ctx, data)\n}\n\nfunc (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {\n\tinfo := schema.QuestionInfoResp{}\n\tinfo.ID = data.ID\n\tif handler.GetEnableShortID(ctx) {\n\t\tinfo.ID = uid.EnShortID(data.ID)\n\t}\n\tinfo.Title = data.Title\n\tinfo.UrlTitle = htmltext.UrlTitle(data.Title)\n\tinfo.Content = data.OriginalText\n\tinfo.HTML = data.ParsedText\n\tinfo.ViewCount = data.ViewCount\n\tinfo.UniqueViewCount = data.UniqueViewCount\n\tinfo.VoteCount = data.VoteCount\n\tinfo.AnswerCount = data.AnswerCount\n\tinfo.CollectionCount = data.CollectionCount\n\tinfo.FollowCount = data.FollowCount\n\tinfo.AcceptedAnswerID = data.AcceptedAnswerID\n\tinfo.LastAnswerID = data.LastAnswerID\n\tinfo.CreateTime = data.CreatedAt.Unix()\n\tinfo.UpdateTime = data.UpdatedAt.Unix()\n\tinfo.PostUpdateTime = data.PostUpdateTime.Unix()\n\tif data.PostUpdateTime.Unix() < 1 {\n\t\tinfo.PostUpdateTime = 0\n\t}\n\tinfo.QuestionUpdateTime = data.UpdatedAt.Unix()\n\tif data.UpdatedAt.Unix() < 1 {\n\t\tinfo.QuestionUpdateTime = 0\n\t}\n\tinfo.Status = data.Status\n\tinfo.Pin = data.Pin\n\tinfo.Show = data.Show\n\tinfo.UserID = data.UserID\n\tinfo.LastEditUserID = data.LastEditUserID\n\tif data.LastAnswerID != \"0\" {\n\t\tanswerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID)\n\t\tif err == nil && exist {\n\t\t\tif answerInfo.LastEditUserID != \"0\" {\n\t\t\t\tinfo.LastAnsweredUserID = answerInfo.LastEditUserID\n\t\t\t} else {\n\t\t\t\tinfo.LastAnsweredUserID = answerInfo.UserID\n\t\t\t}\n\t\t}\n\t}\n\tinfo.Tags = make([]*schema.TagResp, 0)\n\treturn &info\n}\nfunc (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp {\n\tinfo := qs.ShowFormat(ctx, &data.Question)\n\tTags := make([]*schema.TagResp, 0)\n\tfor _, tag := range data.Tags {\n\t\titem := &schema.TagResp{}\n\t\titem.SlugName = tag.SlugName\n\t\titem.DisplayName = tag.DisplayName\n\t\titem.Recommend = tag.Recommend\n\t\titem.Reserved = tag.Reserved\n\t\tTags = append(Tags, item)\n\t}\n\tinfo.Tags = Tags\n\treturn info\n}\n\nfunc (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, answerID, parsedText, originalText string) (string, error) {\n\terr := qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: uid.DeShortID(questionID),\n\t\tFromAnswerID:   uid.DeShortID(answerID),\n\t})\n\tif err != nil {\n\t\treturn parsedText, err\n\t}\n\t// Update the number of question links that have been removed\n\tlinkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, uid.DeShortID(questionID), entity.QuestionLinkStatusDeleted)\n\tif err != nil {\n\t\tlog.Errorf(\"get linked question ids error %v\", err)\n\t} else {\n\t\tfor _, id := range linkedQuestionIDs {\n\t\t\tif err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil {\n\t\t\t\tlog.Errorf(\"update question link count error %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tlinks := checker.GetQuestionLink(originalText)\n\tif len(links) == 0 {\n\t\treturn parsedText, nil\n\t}\n\n\t// get answer ids and question ids\n\tanswerIDs := make([]string, 0, len(links))\n\tquestionIDs := make([]string, 0, len(links))\n\tfor _, link := range links {\n\t\tif link.AnswerID != \"\" {\n\t\t\tanswerIDs = append(answerIDs, link.AnswerID)\n\t\t}\n\t\tif link.QuestionID != \"\" {\n\t\t\tquestionIDs = append(questionIDs, link.QuestionID)\n\t\t}\n\t}\n\n\t// get answer info and build cache\n\tanswerInfoList, err := qs.answerRepo.GetByIDs(ctx, answerIDs...)\n\tif err != nil {\n\t\treturn parsedText, err\n\t}\n\tanswerCache := make(map[string]string, len(answerInfoList))\n\tfor _, ans := range answerInfoList {\n\t\tanswerID := uid.DeShortID(ans.ID)\n\t\tquestionID := ans.QuestionID\n\t\tanswerCache[answerID] = questionID\n\t}\n\n\t// get question info and build cache\n\tquestionInfoList, err := qs.questionRepo.FindByID(ctx, questionIDs)\n\tif err != nil {\n\t\treturn parsedText, err\n\t}\n\tquestionCache := make(map[string]struct{}, len(questionInfoList))\n\tfor _, q := range questionInfoList {\n\t\tquestionID := uid.DeShortID(q.ID)\n\t\tquestionCache[questionID] = struct{}{}\n\t}\n\n\t// process links and generate new QuestionLink\n\tvalidLinks := make([]*entity.QuestionLink, 0, len(links))\n\tfor _, link := range links {\n\t\tlinkQuestionID := uid.DeShortID(link.QuestionID)\n\t\tlinkAnswerID := uid.DeShortID(link.AnswerID)\n\t\t// validate question id\n\t\tif _, exists := questionCache[linkQuestionID]; linkQuestionID != \"0\" && !exists {\n\t\t\tcontinue\n\t\t}\n\n\t\t// validate answer id\n\t\tif linkAnswerID != \"0\" {\n\t\t\tlinkedQuestionID, exists := answerCache[linkAnswerID]\n\t\t\tif !exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if question id is empty, get it from answer cache\n\t\t\tif link.QuestionID == \"\" {\n\t\t\t\tlink.QuestionID = linkedQuestionID\n\t\t\t}\n\t\t}\n\n\t\t// build new link\n\t\tnewLink := &entity.QuestionLink{\n\t\t\tFromQuestionID: uid.DeShortID(questionID),\n\t\t\tFromAnswerID:   uid.DeShortID(answerID),\n\t\t\tToQuestionID:   uid.DeShortID(link.QuestionID),\n\t\t\tToAnswerID:     uid.DeShortID(link.AnswerID),\n\t\t}\n\t\t// replace link in parsed text\n\t\tif link.QuestionID != \"\" {\n\t\t\thtmlLink := fmt.Sprintf(\"<a href=\\\"/questions/%s\\\">#%s</a>\", link.QuestionID, link.QuestionID)\n\t\t\tparsedText = strings.ReplaceAll(parsedText, \"#\"+link.QuestionID, htmlLink)\n\t\t}\n\t\tif link.AnswerID != \"\" {\n\t\t\tlinkedQuestionID := answerCache[linkAnswerID]\n\t\t\thtmlLink := fmt.Sprintf(\"<a href=\\\"/questions/%s/%s\\\">#%s</a>\", linkedQuestionID, link.AnswerID, link.AnswerID)\n\t\t\tparsedText = strings.ReplaceAll(parsedText, \"#\"+link.AnswerID, htmlLink)\n\t\t\tnewLink.ToQuestionID = uid.DeShortID(linkedQuestionID)\n\t\t}\n\t\t// avoid link to self\n\t\tif newLink.FromQuestionID != newLink.ToQuestionID {\n\t\t\tvalidLinks = append(validLinks, newLink)\n\t\t}\n\t}\n\n\t// add new links to repo\n\tif len(validLinks) > 0 {\n\t\terr = qs.questionRepo.LinkQuestion(ctx, validLinks...)\n\t\tif err != nil {\n\t\t\treturn parsedText, err\n\t\t}\n\t}\n\n\t// update question linked count\n\tfor _, link := range validLinks {\n\t\tif len(link.ToQuestionID) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil {\n\t\t\tlog.Errorf(\"update question link count error %v\", err)\n\t\t}\n\t}\n\n\treturn parsedText, nil\n}\n\n// AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question\nfunc (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context,\n\tquestionInfo *entity.Question, closeMsg string) {\n\tquestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg)\n\tif len(questionID) == 0 {\n\t\treturn\n\t}\n\n\tlinkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID)\n\tif err != nil {\n\t\tlog.Errorf(\"get question error %s\", err)\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn\n\t}\n\terr = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: questionInfo.ID,\n\t\tToQuestionID:   linkedQuestion.ID,\n\t\tStatus:         entity.QuestionLinkStatusAvailable,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"link question error %s\", err)\n\t}\n}\n\nfunc (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) {\n\tquestionInfo.ID = uid.DeShortID(questionInfo.ID)\n\tmetaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcloseMsgMeta := &schema.CloseQuestionMeta{}\n\t_ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta)\n\n\tlinkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg)\n\tif len(linkedQuestionID) == 0 {\n\t\treturn\n\t}\n\terr = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{\n\t\tFromQuestionID: questionInfo.ID,\n\t\tToQuestionID:   linkedQuestionID,\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"remove question link error %s\", err)\n\t}\n}\n\nfunc (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) {\n\tsiteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"get site general error %s\", err)\n\t\treturn\n\t}\n\tif !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) {\n\t\treturn\n\t}\n\t// get question id from url\n\t// the url may like: https://xxx.com/questions/D1401/xxx\n\t// the D1401 is question id\n\tquestionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl)\n\tquestionID = strings.TrimPrefix(questionID, \"/questions/\")\n\tt := strings.Split(questionID, \"/\")\n\tif len(t) < 1 {\n\t\treturn \"\"\n\t}\n\tquestionID = t[0]\n\tquestionID = uid.DeShortID(questionID)\n\treturn questionID\n}\n\nfunc (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, error) {\n\tsiteInfo, err := qs.siteInfoService.GetSiteQuestion(ctx)\n\tif err != nil {\n\t\treturn 6, err\n\t}\n\treturn siteInfo.MinimumContent, nil\n}\n"
  },
  {
    "path": "internal/service/rank/rank_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage rank\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_type\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/internal/service/role\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n)\n\nconst (\n\tPermissionPrefix = \"rank.\"\n)\n\ntype UserRankRepo interface {\n\tGetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error)\n\tCheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error)\n\tChangeUserRank(ctx context.Context, session *xorm.Session,\n\t\tuserID string, userCurrentScore, deltaRank int) (err error)\n\tTriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error)\n\tUserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error)\n}\n\n// RankService rank service\ntype RankService struct {\n\tuserCommon        *usercommon.UserCommon\n\tconfigService     *config.ConfigService\n\tuserRankRepo      UserRankRepo\n\tobjectInfoService *object_info.ObjService\n\troleService       *role.UserRoleRelService\n\trolePowerService  *role.RolePowerRelService\n}\n\n// NewRankService new rank service\nfunc NewRankService(\n\tuserCommon *usercommon.UserCommon,\n\tuserRankRepo UserRankRepo,\n\tobjectInfoService *object_info.ObjService,\n\troleService *role.UserRoleRelService,\n\trolePowerService *role.RolePowerRelService,\n\tconfigService *config.ConfigService) *RankService {\n\treturn &RankService{\n\t\tuserCommon:        userCommon,\n\t\tconfigService:     configService,\n\t\tuserRankRepo:      userRankRepo,\n\t\tobjectInfoService: objectInfoService,\n\t\troleService:       roleService,\n\t\trolePowerService:  rolePowerService,\n\t}\n}\n\n// CheckOperationPermission verify that the user has permission\nfunc (rs *RankService) CheckOperationPermission(ctx context.Context, userID string, action string, objectID string) (\n\tcan bool, err error) {\n\tif len(userID) == 0 {\n\t\treturn false, nil\n\t}\n\n\t// get the rank of the current user\n\tuserInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !exist {\n\t\treturn false, nil\n\t}\n\tpowerMapping := rs.getUserPowerMapping(ctx, userID)\n\tif powerMapping[action] {\n\t\treturn true, nil\n\t}\n\n\tif len(objectID) > 0 {\n\t\tobjectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)\n\t\tif err != nil {\n\t\t\treturn can, err\n\t\t}\n\t\t// if the user is this object creator, the user can operate this object.\n\t\tif objectInfo != nil &&\n\t\t\tobjectInfo.ObjectCreatorUserID == userID {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tcan, _ = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)\n\treturn can, nil\n}\n\n// CheckOperationPermissionsForRanks verify that the user has permission\nfunc (rs *RankService) CheckOperationPermissionsForRanks(ctx context.Context, userID string, actions []string) (\n\tcan []bool, requireRanks []int, err error) {\n\tcan = make([]bool, len(actions))\n\trequireRanks = make([]int, len(actions))\n\tif len(userID) == 0 {\n\t\treturn can, requireRanks, nil\n\t}\n\n\t// get the rank of the current user\n\tuserInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)\n\tif err != nil {\n\t\treturn can, requireRanks, err\n\t}\n\tif !exist {\n\t\treturn can, requireRanks, nil\n\t}\n\n\tpowerMapping := rs.getUserPowerMapping(ctx, userID)\n\tfor idx, action := range actions {\n\t\tif powerMapping[action] {\n\t\t\tcan[idx] = true\n\t\t\tcontinue\n\t\t}\n\t\tmeetRank, requireRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)\n\t\tcan[idx] = meetRank\n\t\trequireRanks[idx] = requireRank\n\t}\n\treturn can, requireRanks, nil\n}\n\n// CheckOperationPermissions verify that the user has permission\nfunc (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string) (\n\tcan []bool, err error) {\n\tcan, _, err = rs.CheckOperationPermissionsForRanks(ctx, userID, actions)\n\treturn can, err\n}\n\n// CheckOperationObjectOwner check operation object owner\nfunc (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool {\n\tobjectID = uid.DeShortID(objectID)\n\tobjectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false\n\t}\n\t// if the user is this object creator, the user can operate this object.\n\tif objectInfo != nil &&\n\t\tobjectInfo.ObjectCreatorUserID == userID {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// CheckVotePermission verify that the user has vote permission\nfunc (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) (\n\tcan bool, needRank int, err error) {\n\tif len(userID) == 0 || len(objectID) == 0 {\n\t\treturn false, 0, nil\n\t}\n\n\t// get the rank of the current user\n\tuserInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)\n\tif err != nil {\n\t\treturn can, 0, err\n\t}\n\tif !exist {\n\t\treturn can, 0, nil\n\t}\n\tobjectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)\n\tif err != nil {\n\t\treturn can, 0, err\n\t}\n\taction := \"\"\n\tswitch objectInfo.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tif voteUp {\n\t\t\taction = permission.QuestionVoteUp\n\t\t} else {\n\t\t\taction = permission.QuestionVoteDown\n\t\t}\n\tcase constant.AnswerObjectType:\n\t\tif voteUp {\n\t\t\taction = permission.AnswerVoteUp\n\t\t} else {\n\t\t\taction = permission.AnswerVoteDown\n\t\t}\n\tcase constant.CommentObjectType:\n\t\tif voteUp {\n\t\t\taction = permission.CommentVoteUp\n\t\t} else {\n\t\t\taction = permission.CommentVoteDown\n\t\t}\n\t}\n\tpowerMapping := rs.getUserPowerMapping(ctx, userID)\n\tif powerMapping[action] {\n\t\treturn true, 0, nil\n\t}\n\tcan, needRank = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)\n\treturn can, needRank, nil\n}\n\n// getUserPowerMapping get user power mapping\nfunc (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (powerMapping map[string]bool) {\n\tpowerMapping = make(map[string]bool, 0)\n\tuserRole, err := rs.roleService.GetUserRole(ctx, userID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn powerMapping\n\t}\n\tpowers, err := rs.rolePowerService.GetRolePowerList(ctx, userRole)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn powerMapping\n\t}\n\n\tfor _, power := range powers {\n\t\tpowerMapping[power] = true\n\t}\n\treturn powerMapping\n}\n\n// checkUserRank verify that the user meets the prestige criteria\nfunc (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) (\n\tcan bool, rank int) {\n\t// get the amount of rank required for the current operation\n\trequireRank, err := rs.configService.GetIntValue(ctx, action)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false, requireRank\n\t}\n\tif userRank < requireRank || requireRank < 0 {\n\t\tlog.Debugf(\"user %s want to do action %s, but rank %d < %d\",\n\t\t\tuserID, action, userRank, requireRank)\n\t\treturn false, requireRank\n\t}\n\treturn true, requireRank\n}\n\n// GetRankPersonalPage get personal comment list page\nfunc (rs *RankService) GetRankPersonalPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tif plugin.RankAgentEnabled() {\n\t\treturn pager.NewPageModel(0, []string{}), nil\n\t}\n\tif len(req.Username) > 0 {\n\t\tuserInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t\t}\n\t\treq.UserID = userInfo.ID\n\t}\n\tif len(req.UserID) == 0 {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tuserRankPage, total, err := rs.userRankRepo.UserRankPage(ctx, req.UserID, req.Page, req.PageSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := rs.decorateRankPersonalPageResp(ctx, userRankPage)\n\treturn pager.NewPageModel(total, resp), nil\n}\n\nfunc (rs *RankService) decorateRankPersonalPageResp(\n\tctx context.Context, userRankPage []*entity.Activity) []*schema.GetRankPersonalPageResp {\n\tresp := make([]*schema.GetRankPersonalPageResp, 0)\n\tlang := handler.GetLangByCtx(ctx)\n\n\tfor _, userRankInfo := range userRankPage {\n\t\tif len(userRankInfo.ObjectID) == 0 || userRankInfo.ObjectID == \"0\" {\n\t\t\tcontinue\n\t\t}\n\t\tobjInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tcommentResp := &schema.GetRankPersonalPageResp{\n\t\t\tCreatedAt:  userRankInfo.CreatedAt.Unix(),\n\t\t\tObjectID:   userRankInfo.ObjectID,\n\t\t\tReputation: userRankInfo.Rank,\n\t\t}\n\t\tcfg, err := rs.configService.GetConfigByID(ctx, userRankInfo.ActivityType)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tcommentResp.RankType = translator.Tr(lang, activity_type.ActivityTypeFlagMapping[cfg.Key])\n\t\tcommentResp.ObjectType = objInfo.ObjectType\n\t\tcommentResp.Title = objInfo.Title\n\t\tcommentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)\n\t\tcommentResp.Content = objInfo.Content\n\t\tif objInfo.QuestionStatus == entity.QuestionStatusDeleted {\n\t\t\tcommentResp.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey)\n\t\t}\n\t\tcommentResp.QuestionID = objInfo.QuestionID\n\t\tcommentResp.AnswerID = objInfo.AnswerID\n\t\tresp = append(resp, commentResp)\n\t}\n\treturn resp\n}\n"
  },
  {
    "path": "internal/service/reason/reason_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage reason\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/reason_common\"\n)\n\ntype ReasonService struct {\n\treasonRepo reason_common.ReasonRepo\n}\n\nfunc NewReasonService(reasonRepo reason_common.ReasonRepo) *ReasonService {\n\treturn &ReasonService{\n\t\treasonRepo: reasonRepo,\n\t}\n}\n\nfunc (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []*schema.ReasonItem, err error) {\n\treturn rs.reasonRepo.ListReasons(ctx, req.ObjectType, req.Action)\n}\n"
  },
  {
    "path": "internal/service/reason_common/reason.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage reason_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n)\n\ntype ReasonRepo interface {\n\tListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error)\n}\n"
  },
  {
    "path": "internal/service/report/report_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage report\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/apache/answer/internal/service/eventqueue\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/report_common\"\n\t\"github.com/apache/answer/internal/service/report_handle\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/net/context\"\n)\n\n// ReportService user service\ntype ReportService struct {\n\treportRepo        report_common.ReportRepo\n\tobjectInfoService *object_info.ObjService\n\tcommonUser        *usercommon.UserCommon\n\tanswerRepo        answercommon.AnswerRepo\n\tquestionRepo      questioncommon.QuestionRepo\n\tcommentCommonRepo comment_common.CommentCommonRepo\n\treportHandle      *report_handle.ReportHandle\n\tconfigService     *config.ConfigService\n\teventQueueService eventqueue.Service\n}\n\n// NewReportService new report service\nfunc NewReportService(\n\treportRepo report_common.ReportRepo,\n\tobjectInfoService *object_info.ObjService,\n\tcommonUser *usercommon.UserCommon,\n\tanswerRepo answercommon.AnswerRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\tcommentCommonRepo comment_common.CommentCommonRepo,\n\treportHandle *report_handle.ReportHandle,\n\tconfigService *config.ConfigService,\n\teventQueueService eventqueue.Service,\n) *ReportService {\n\treturn &ReportService{\n\t\treportRepo:        reportRepo,\n\t\tobjectInfoService: objectInfoService,\n\t\tcommonUser:        commonUser,\n\t\tanswerRepo:        answerRepo,\n\t\tquestionRepo:      questionRepo,\n\t\tcommentCommonRepo: commentCommonRepo,\n\t\treportHandle:      reportHandle,\n\t\tconfigService:     configService,\n\t\teventQueueService: eventQueueService,\n\t}\n}\n\n// AddReport add report\nfunc (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq) (err error) {\n\tobjectTypeNumber, err := obj.GetObjectTypeNumberByObjectID(req.ObjectID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tobjInfo, err := rs.objectInfoService.GetInfo(ctx, req.ObjectID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif objInfo.IsDeleted() {\n\t\treturn errors.BadRequest(reason.NewObjectAlreadyDeleted)\n\t}\n\n\tcf, err := rs.configService.GetConfigByID(ctx, req.ReportType)\n\tif err != nil || cf == nil {\n\t\treturn errors.BadRequest(reason.ReportNotFound)\n\t}\n\tif cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.Content) {\n\t\treturn errors.BadRequest(reason.InvalidURLError)\n\t}\n\n\treport := &entity.Report{\n\t\tUserID:         req.UserID,\n\t\tReportedUserID: objInfo.ObjectCreatorUserID,\n\t\tObjectID:       req.ObjectID,\n\t\tObjectType:     objectTypeNumber,\n\t\tReportType:     req.ReportType,\n\t\tContent:        req.Content,\n\t\tStatus:         entity.ReportStatusPending,\n\t}\n\terr = rs.reportRepo.AddReport(ctx, report)\n\tif err != nil {\n\t\treturn err\n\t}\n\trs.sendEvent(ctx, report, objInfo)\n\treturn nil\n}\n\n// GetUnreviewedReportPostPage get unreviewed report post page\nfunc (rs *ReportService) GetUnreviewedReportPostPage(ctx context.Context, req *schema.GetUnreviewedReportPostPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tif !req.IsAdmin {\n\t\treturn pager.NewPageModel(0, make([]*schema.GetReportListPageResp, 0)), nil\n\t}\n\tlang := handler.GetLangByCtx(ctx)\n\treports, total, err := rs.reportRepo.GetReportListPage(ctx, &schema.GetReportListPageDTO{\n\t\tPage:     req.Page,\n\t\tPageSize: 1,\n\t\tStatus:   entity.ReportStatusPending,\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresp := make([]*schema.GetReportListPageResp, 0)\n\tfor _, report := range reports {\n\t\tinfo, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, report.ObjectID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"GetUnreviewedRevisionInfo failed, err: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tr := &schema.GetReportListPageResp{\n\t\t\tFlagID:           report.ID,\n\t\t\tCreatedAt:        info.CreatedAt,\n\t\t\tObjectID:         info.ObjectID,\n\t\t\tObjectType:       info.ObjectType,\n\t\t\tQuestionID:       info.QuestionID,\n\t\t\tAnswerID:         info.AnswerID,\n\t\t\tCommentID:        info.CommentID,\n\t\t\tTitle:            info.Title,\n\t\t\tUrlTitle:         htmltext.UrlTitle(info.Title),\n\t\t\tOriginalText:     info.Content,\n\t\t\tParsedText:       info.Html,\n\t\t\tAnswerCount:      info.AnswerCount,\n\t\t\tAnswerAccepted:   info.AnswerAccepted,\n\t\t\tTags:             info.Tags,\n\t\t\tSubmitAt:         report.CreatedAt.Unix(),\n\t\t\tObjectStatus:     info.Status,\n\t\t\tObjectShowStatus: info.ShowStatus,\n\t\t\tReasonContent:    report.Content,\n\t\t}\n\n\t\t// get user info\n\t\tuserInfo, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID)\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"user not found by id: %s, err: %v\", info.ObjectCreatorUserID, e)\n\t\t}\n\t\tif exists {\n\t\t\t_ = copier.Copy(&r.AuthorUserInfo, userInfo)\n\t\t}\n\n\t\t// get submitter info\n\t\tsubmitter, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, report.ReportedUserID)\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"user not found by id: %s, err: %v\", info.ObjectCreatorUserID, e)\n\t\t}\n\t\tif exists {\n\t\t\t_ = copier.Copy(&r.SubmitterUser, submitter)\n\t\t}\n\n\t\tif report.ReportType > 0 {\n\t\t\tr.Reason = &schema.ReasonItem{ReasonType: report.ReportType}\n\t\t\tcf, err := rs.configService.GetConfigByID(ctx, report.ReportType)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t} else {\n\t\t\t\t_ = json.Unmarshal([]byte(cf.Value), r.Reason)\n\t\t\t\tr.Reason.Translate(cf.Key, lang)\n\t\t\t}\n\t\t}\n\t\tresp = append(resp, r)\n\t}\n\treturn pager.NewPageModel(total, resp), nil\n}\n\n// ReviewReport review report\nfunc (rs *ReportService) ReviewReport(ctx context.Context, req *schema.ReviewReportReq) (err error) {\n\treport, exist, err := rs.reportRepo.GetByID(ctx, req.FlagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.NotFound(reason.ReportNotFound)\n\t}\n\t// check if handle or not\n\tif report.Status != entity.ReportStatusPending {\n\t\treturn nil\n\t}\n\n\t// ignore this report\n\tif req.OperationType == constant.ReportOperationIgnoreReport {\n\t\treturn rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusIgnore)\n\t}\n\n\tif err = rs.reportHandle.UpdateReportedObject(ctx, report, req); err != nil {\n\t\treturn\n\t}\n\n\treturn rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusCompleted)\n}\n\nfunc (rs *ReportService) sendEvent(ctx context.Context,\n\treport *entity.Report, objectInfo *schema.SimpleObjectInfo) {\n\tvar event *schema.EventMsg\n\tswitch objectInfo.ObjectType {\n\tcase constant.QuestionObjectType:\n\t\tevent = schema.NewEvent(constant.EventQuestionFlag, report.UserID).TID(objectInfo.QuestionID).\n\t\t\tQID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID)\n\tcase constant.AnswerObjectType:\n\t\tevent = schema.NewEvent(constant.EventAnswerFlag, report.UserID).TID(objectInfo.AnswerID).\n\t\t\tAID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID)\n\tcase constant.CommentObjectType:\n\t\tevent = schema.NewEvent(constant.EventCommentFlag, report.UserID).TID(objectInfo.CommentID).\n\t\t\tCID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID)\n\tdefault:\n\t\treturn\n\t}\n\trs.eventQueueService.Send(ctx, event)\n}\n"
  },
  {
    "path": "internal/service/report_common/report_common.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage report_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n)\n\n// ReportRepo report repository\ntype ReportRepo interface {\n\tAddReport(ctx context.Context, report *entity.Report) (err error)\n\tGetReportListPage(ctx context.Context, query *schema.GetReportListPageDTO) (\n\t\treports []*entity.Report, total int64, err error)\n\tGetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error)\n\tUpdateStatus(ctx context.Context, id string, status int) (err error)\n\tGetReportCount(ctx context.Context) (count int64, err error)\n}\n"
  },
  {
    "path": "internal/service/report_handle/report_handle.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage report_handle\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/comment\"\n\t\"github.com/apache/answer/internal/service/content\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/obj\"\n)\n\ntype ReportHandle struct {\n\tquestionService *content.QuestionService\n\tanswerService   *content.AnswerService\n\tcommentService  *comment.CommentService\n}\n\nfunc NewReportHandle(\n\tquestionService *content.QuestionService,\n\tanswerService *content.AnswerService,\n\tcommentService *comment.CommentService,\n) *ReportHandle {\n\treturn &ReportHandle{\n\t\tquestionService: questionService,\n\t\tanswerService:   answerService,\n\t\tcommentService:  commentService,\n\t}\n}\n\n// UpdateReportedObject this handle object status\nfunc (rh *ReportHandle) UpdateReportedObject(ctx context.Context,\n\treport *entity.Report, req *schema.ReviewReportReq) (err error) {\n\tobjectKey, err := obj.GetObjectTypeStrByObjectID(report.ObjectID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch objectKey {\n\tcase constant.QuestionObjectType:\n\t\terr = rh.updateReportedQuestionReport(ctx, report, req)\n\tcase constant.AnswerObjectType:\n\t\terr = rh.updateReportedAnswerReport(ctx, report, req)\n\tcase constant.CommentObjectType:\n\t\terr = rh.updateReportedCommentReport(ctx, report, req)\n\t}\n\treturn\n}\n\nfunc (rh *ReportHandle) updateReportedQuestionReport(ctx context.Context,\n\treport *entity.Report, req *schema.ReviewReportReq) (err error) {\n\tswitch req.OperationType {\n\tcase constant.ReportOperationUnlistPost:\n\t\terr = rh.questionService.OperationQuestion(ctx, &schema.OperationQuestionReq{\n\t\t\tID: report.ObjectID, Operation: schema.QuestionOperationHide, UserID: req.UserID})\n\tcase constant.ReportOperationDeletePost:\n\t\terr = rh.questionService.RemoveQuestion(ctx, &schema.RemoveQuestionReq{\n\t\t\tID: report.ObjectID, UserID: req.UserID, IsAdmin: true})\n\tcase constant.ReportOperationClosePost:\n\t\terr = rh.questionService.CloseQuestion(ctx, &schema.CloseQuestionReq{\n\t\t\tID:        report.ObjectID,\n\t\t\tCloseType: req.CloseType,\n\t\t\tCloseMsg:  req.CloseMsg,\n\t\t\tUserID:    req.UserID,\n\t\t})\n\tcase constant.ReportOperationEditPost:\n\t\t_, err = rh.questionService.UpdateQuestion(ctx, &schema.QuestionUpdate{\n\t\t\tID:           report.ObjectID,\n\t\t\tTitle:        req.Title,\n\t\t\tContent:      req.Content,\n\t\t\tHTML:         converter.Markdown2HTML(req.Content),\n\t\t\tTags:         req.Tags,\n\t\t\tUserID:       req.UserID,\n\t\t\tNoNeedReview: true,\n\t\t})\n\t}\n\treturn\n}\n\nfunc (rh *ReportHandle) updateReportedAnswerReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) {\n\tswitch req.OperationType {\n\tcase constant.ReportOperationDeletePost:\n\t\terr = rh.answerService.RemoveAnswer(ctx, &schema.RemoveAnswerReq{\n\t\t\tID: report.ObjectID, UserID: req.UserID})\n\tcase constant.ReportOperationEditPost:\n\t\t_, err = rh.answerService.Update(ctx, &schema.AnswerUpdateReq{\n\t\t\tID:           report.ObjectID,\n\t\t\tTitle:        req.Title,\n\t\t\tContent:      req.Content,\n\t\t\tHTML:         converter.Markdown2HTML(req.Content),\n\t\t\tUserID:       req.UserID,\n\t\t\tNoNeedReview: true,\n\t\t})\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (rh *ReportHandle) updateReportedCommentReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) {\n\tswitch req.OperationType {\n\tcase constant.ReportOperationDeletePost:\n\t\terr = rh.commentService.RemoveComment(ctx, &schema.RemoveCommentReq{\n\t\t\tCommentID: report.ObjectID, UserID: req.UserID})\n\tcase constant.ReportOperationEditPost:\n\t\t_, err = rh.commentService.UpdateComment(ctx, &schema.UpdateCommentReq{\n\t\t\tCommentID:    report.ObjectID,\n\t\t\tOriginalText: req.Content,\n\t\t\tParsedText:   converter.Markdown2HTML(req.Content),\n\t\t\tUserID:       req.UserID,\n\t\t})\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/review/review_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage review\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\tcommentcommon \"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/noticequeue\"\n\t\"github.com/apache/answer/internal/service/object_info\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// ReviewRepo review repository\ntype ReviewRepo interface {\n\tAddReview(ctx context.Context, review *entity.Review) (err error)\n\tUpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error)\n\tGetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error)\n\tGetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error)\n\tGetReviewCount(ctx context.Context, status int) (count int64, err error)\n\tGetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error)\n}\n\n// ReviewService user service\ntype ReviewService struct {\n\treviewRepo                       ReviewRepo\n\tobjectInfoService                *object_info.ObjService\n\tuserCommon                       *usercommon.UserCommon\n\tuserRepo                         usercommon.UserRepo\n\tquestionRepo                     questioncommon.QuestionRepo\n\tanswerRepo                       answercommon.AnswerRepo\n\tuserRoleService                  *role.UserRoleRelService\n\ttagCommon                        *tagcommon.TagCommonService\n\tquestionCommon                   *questioncommon.QuestionCommon\n\texternalNotificationQueueService noticequeue.ExternalService\n\tnotificationQueueService         noticequeue.Service\n\tsiteInfoService                  siteinfo_common.SiteInfoCommonService\n\tcommentCommonRepo                commentcommon.CommentCommonRepo\n}\n\n// NewReviewService new review service\nfunc NewReviewService(\n\treviewRepo ReviewRepo,\n\tobjectInfoService *object_info.ObjService,\n\tuserCommon *usercommon.UserCommon,\n\tuserRepo usercommon.UserRepo,\n\tquestionRepo questioncommon.QuestionRepo,\n\tanswerRepo answercommon.AnswerRepo,\n\tuserRoleService *role.UserRoleRelService,\n\texternalNotificationQueueService noticequeue.ExternalService,\n\ttagCommon *tagcommon.TagCommonService,\n\tquestionCommon *questioncommon.QuestionCommon,\n\tnotificationQueueService noticequeue.Service,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tcommentCommonRepo commentcommon.CommentCommonRepo,\n) *ReviewService {\n\treturn &ReviewService{\n\t\treviewRepo:                       reviewRepo,\n\t\tobjectInfoService:                objectInfoService,\n\t\tuserCommon:                       userCommon,\n\t\tuserRepo:                         userRepo,\n\t\tquestionRepo:                     questionRepo,\n\t\tanswerRepo:                       answerRepo,\n\t\tuserRoleService:                  userRoleService,\n\t\texternalNotificationQueueService: externalNotificationQueueService,\n\t\ttagCommon:                        tagCommon,\n\t\tquestionCommon:                   questionCommon,\n\t\tnotificationQueueService:         notificationQueueService,\n\t\tsiteInfoService:                  siteInfoService,\n\t\tcommentCommonRepo:                commentCommonRepo,\n\t}\n}\n\n// AddQuestionReview add review for question if needed\nfunc (cs *ReviewService) AddQuestionReview(ctx context.Context,\n\tquestion *entity.Question, tags []*schema.TagItem, ip, ua string) (questionStatus int) {\n\treviewContent := &plugin.ReviewContent{\n\t\tObjectType: constant.QuestionObjectType,\n\t\tTitle:      question.Title,\n\t\tContent:    question.ParsedText,\n\t\tIP:         ip,\n\t\tUserAgent:  ua,\n\t}\n\tfor _, tag := range tags {\n\t\treviewContent.Tags = append(reviewContent.Tags, tag.SlugName)\n\t}\n\treviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID)\n\treviewStatus := cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent)\n\tswitch reviewStatus {\n\tcase plugin.ReviewStatusApproved:\n\t\tquestionStatus = entity.QuestionStatusAvailable\n\tcase plugin.ReviewStatusNeedReview:\n\t\tquestionStatus = entity.QuestionStatusPending\n\tcase plugin.ReviewStatusDeleteDirectly:\n\t\tquestionStatus = entity.QuestionStatusDeleted\n\tdefault:\n\t\tquestionStatus = entity.QuestionStatusAvailable\n\t}\n\treturn questionStatus\n}\n\n// AddAnswerReview add review for answer if needed\nfunc (cs *ReviewService) AddAnswerReview(ctx context.Context,\n\tanswer *entity.Answer, ip, ua string) (answerStatus int) {\n\treviewContent := &plugin.ReviewContent{\n\t\tObjectType: constant.AnswerObjectType,\n\t\tContent:    answer.ParsedText,\n\t\tIP:         ip,\n\t\tUserAgent:  ua,\n\t}\n\treviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID)\n\treviewStatus := cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent)\n\tswitch reviewStatus {\n\tcase plugin.ReviewStatusApproved:\n\t\tanswerStatus = entity.AnswerStatusAvailable\n\tcase plugin.ReviewStatusNeedReview:\n\t\tanswerStatus = entity.AnswerStatusPending\n\tcase plugin.ReviewStatusDeleteDirectly:\n\t\tanswerStatus = entity.AnswerStatusDeleted\n\tdefault:\n\t\tanswerStatus = entity.AnswerStatusAvailable\n\t}\n\treturn answerStatus\n}\n\n// AddCommentReview add review for comment if needed\nfunc (cs *ReviewService) AddCommentReview(ctx context.Context,\n\tcomment *entity.Comment, ip, ua string) (commentStatus int) {\n\treviewContent := &plugin.ReviewContent{\n\t\tObjectType: constant.CommentObjectType,\n\t\tContent:    comment.ParsedText,\n\t\tIP:         ip,\n\t\tUserAgent:  ua,\n\t}\n\treviewContent.Author = cs.getReviewContentAuthorInfo(ctx, comment.UserID)\n\treviewStatus := cs.callPluginToReview(ctx, comment.UserID, comment.ID, reviewContent)\n\tswitch reviewStatus {\n\tcase plugin.ReviewStatusApproved:\n\t\tcommentStatus = entity.CommentStatusAvailable\n\tcase plugin.ReviewStatusNeedReview:\n\t\tcommentStatus = entity.CommentStatusPending\n\tcase plugin.ReviewStatusDeleteDirectly:\n\t\tcommentStatus = entity.CommentStatusDeleted\n\tdefault:\n\t\tcommentStatus = entity.CommentStatusAvailable\n\t}\n\treturn commentStatus\n}\n\n// get review content author info\nfunc (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) {\n\tuser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"get user info failed, err: %v\", err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Errorf(\"user not found by id: %s\", userID)\n\t\treturn\n\t}\n\tauthor.Rank = user.Rank\n\tauthor.ApprovedQuestionAmount, _ = cs.questionRepo.GetUserQuestionCount(ctx, userID, 0)\n\tauthor.ApprovedAnswerAmount, _ = cs.answerRepo.GetCountByUserID(ctx, userID)\n\tauthor.Role, _ = cs.userRoleService.GetUserRole(ctx, userID)\n\treturn\n}\n\n// call plugin to review\nfunc (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string,\n\treviewContent *plugin.ReviewContent) (reviewStatus plugin.ReviewStatus) {\n\t// As default, no need review\n\treviewStatus = plugin.ReviewStatusApproved\n\tobjectID = uid.DeShortID(objectID)\n\n\tr := &entity.Review{\n\t\tUserID:         userID,\n\t\tObjectID:       objectID,\n\t\tObjectType:     constant.ObjectTypeStrMapping[reviewContent.ObjectType],\n\t\tReviewerUserID: \"0\",\n\t\tStatus:         entity.ReviewStatusPending,\n\t}\n\tif siteInterface, _ := cs.siteInfoService.GetSiteInterface(ctx); siteInterface != nil {\n\t\treviewContent.Language = siteInterface.Language\n\t}\n\n\t_ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error {\n\t\t// If one of the reviewer plugin return false, then the review is not approved\n\t\tif reviewStatus != plugin.ReviewStatusApproved {\n\t\t\treturn nil\n\t\t}\n\t\tif result := reviewer.Review(reviewContent); !result.Approved {\n\t\t\treviewStatus = result.ReviewStatus\n\t\t\tr.Reason = result.Reason\n\t\t\tr.Submitter = reviewer.Info().SlugName\n\t\t}\n\t\treturn nil\n\t})\n\n\tif reviewStatus == plugin.ReviewStatusNeedReview {\n\t\tif err := cs.reviewRepo.AddReview(ctx, r); err != nil {\n\t\t\tlog.Errorf(\"add review failed, err: %v\", err)\n\t\t}\n\t}\n\treturn reviewStatus\n}\n\n// UpdateReview update review\nfunc (cs *ReviewService) UpdateReview(ctx context.Context, req *schema.UpdateReviewReq) (err error) {\n\treview, exist, err := cs.reviewRepo.GetReview(ctx, req.ReviewID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\tif review.Status != entity.ReviewStatusPending {\n\t\treturn nil\n\t}\n\n\tif err = cs.updateObjectStatus(ctx, review, req.IsApprove()); err != nil {\n\t\treturn err\n\t}\n\n\tif req.IsApprove() {\n\t\terr = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusApproved)\n\t} else {\n\t\terr = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusRejected)\n\t}\n\treturn\n}\n\n// update object status\nfunc (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.Review, isApprove bool) (err error) {\n\tobjectType := constant.ObjectTypeNumberMapping[review.ObjectType]\n\tswitch objectType {\n\tcase constant.QuestionObjectType:\n\t\tquestionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, review.ObjectID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t\t}\n\t\tif isApprove {\n\t\t\tquestionInfo.Status = entity.QuestionStatusAvailable\n\t\t} else {\n\t\t\tquestionInfo.Status = entity.QuestionStatusDeleted\n\t\t}\n\t\tif err := cs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif isApprove {\n\t\t\ttags, err := cs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"get question tags failed, err: %v\", err)\n\t\t\t}\n\t\t\tcs.externalNotificationQueueService.Send(ctx,\n\t\t\t\tschema.CreateNewQuestionNotificationMsg(questionInfo.ID, questionInfo.Title, questionInfo.UserID, tags))\n\t\t}\n\t\tuserQuestionCount, err := cs.questionRepo.GetUserQuestionCount(ctx, questionInfo.UserID, 0)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get user question count failed, err: %v\", err)\n\t\t} else {\n\t\t\terr = cs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"update user question count failed, err: %v\", err)\n\t\t\t}\n\t\t}\n\tcase constant.AnswerObjectType:\n\t\tanswerInfo, exist, err := cs.answerRepo.GetAnswer(ctx, review.ObjectID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t\t}\n\t\tif isApprove {\n\t\t\tanswerInfo.Status = entity.AnswerStatusAvailable\n\t\t} else {\n\t\t\tanswerInfo.Status = entity.AnswerStatusDeleted\n\t\t}\n\t\tif err := cs.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, answerInfo.Status); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tquestionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t\t}\n\t\tif isApprove {\n\t\t\tcs.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, answerInfo.ID,\n\t\t\t\tanswerInfo.UserID, questionInfo.Title, answerInfo.OriginalText)\n\t\t}\n\t\tif err := cs.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil {\n\t\t\tlog.Errorf(\"update question answer count failed, err: %v\", err)\n\t\t}\n\t\tif err := cs.questionCommon.UpdateLastAnswer(ctx, answerInfo.QuestionID, uid.DeShortID(answerInfo.ID)); err != nil {\n\t\t\tlog.Errorf(\"update question last answer failed, err: %v\", err)\n\t\t}\n\t\tuserAnswerCount, err := cs.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"get user answer count failed, err: %v\", err)\n\t\t} else {\n\t\t\terr = cs.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"update user answer count failed, err: %v\", err)\n\t\t\t}\n\t\t}\n\tcase constant.CommentObjectType:\n\t\tcommentInfo, exist, err := cs.commentCommonRepo.GetCommentWithoutStatus(ctx, review.ObjectID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t\t}\n\t\tif isApprove {\n\t\t\tcommentInfo.Status = entity.CommentStatusAvailable\n\t\t} else {\n\t\t\tcommentInfo.Status = entity.CommentStatusDeleted\n\t\t}\n\t\tif err := cs.commentCommonRepo.UpdateCommentStatus(ctx, commentInfo.ID, commentInfo.Status); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, exist, err = cs.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !exist {\n\t\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t\t}\n\t\tif isApprove {\n\t\t\tcs.notificationCommentOnTheQuestion(ctx, commentInfo)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context,\n\tquestionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {\n\t// If the question is answered by me, there is no notification for myself.\n\tif questionUserID == answerUserID {\n\t\treturn\n\t}\n\tmsg := &schema.NotificationMsg{\n\t\tTriggerUserID:  answerUserID,\n\t\tReceiverUserID: questionUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       answerID,\n\t}\n\tmsg.ObjectType = constant.AnswerObjectType\n\tmsg.NotificationAction = constant.NotificationAnswerTheQuestion\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", questionUserID)\n\t\treturn\n\t}\n\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewAnswerTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tAnswerID:        answerID,\n\t\tAnswerSummary:   answerSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tanswerUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, answerUserID)\n\tif answerUser != nil {\n\t\trawData.AnswerUserDisplayName = answerUser.DisplayName\n\t}\n\texternalNotificationMsg.NewAnswerTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *ReviewService) notificationCommentOnTheQuestion(ctx context.Context, comment *entity.Comment) {\n\tobjInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif objInfo.IsDeleted() {\n\t\tlog.Error(\"object already deleted\")\n\t\treturn\n\t}\n\tobjInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)\n\tobjInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)\n\tobjInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)\n\n\t// The priority of the notification\n\t// 1. reply to user\n\t// 2. comment mention to user\n\t// 3. answer or question was commented\n\talreadyNotifiedUserID := make(map[string]bool)\n\n\t// get reply user info\n\treplyUserID := comment.GetReplyUserID()\n\tif len(replyUserID) > 0 && replyUserID != comment.UserID {\n\t\treplyUser, _, err := cs.userCommon.GetUserBasicInfoByID(ctx, replyUserID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tcs.notificationCommentReply(ctx, replyUser.ID, comment.ID, comment.UserID,\n\t\t\tobjInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t\talreadyNotifiedUserID[replyUser.ID] = true\n\t\treturn\n\t}\n\n\tmentionUsernameList := comment.GetMentionUsernameList()\n\tif len(mentionUsernameList) > 0 {\n\t\talreadyNotifiedUserIDs := cs.notificationMention(\n\t\t\tctx, mentionUsernameList, comment.ID, comment.UserID, alreadyNotifiedUserID)\n\t\tfor _, userID := range alreadyNotifiedUserIDs {\n\t\t\talreadyNotifiedUserID[userID] = true\n\t\t}\n\t\treturn\n\t}\n\n\tif objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {\n\t\tcs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,\n\t\t\tobjInfo.QuestionID, objInfo.Title, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {\n\t\tcs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,\n\t\t\tobjInfo.ObjectCreatorUserID, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, \"...\", 240))\n\t}\n}\n\nfunc (cs *ReviewService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID,\n\tquestionID, questionTitle, commentSummary string) {\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: replyUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationReplyToYou\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// Send external notification.\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", replyUserID)\n\t\treturn\n\t}\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *ReviewService) notificationMention(\n\tctx context.Context, mentionUsernameList []string, commentID, commentUserID string,\n\talreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {\n\tfor _, username := range mentionUsernameList {\n\t\tuserInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tif exist && !alreadyNotifiedUserID[userInfo.ID] {\n\t\t\tmsg := &schema.NotificationMsg{\n\t\t\t\tReceiverUserID: userInfo.ID,\n\t\t\t\tTriggerUserID:  commentUserID,\n\t\t\t\tType:           schema.NotificationTypeInbox,\n\t\t\t\tObjectID:       commentID,\n\t\t\t}\n\t\t\tmsg.ObjectType = constant.CommentObjectType\n\t\t\tmsg.NotificationAction = constant.NotificationMentionYou\n\t\t\tcs.notificationQueueService.Send(ctx, msg)\n\t\t\talreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)\n\t\t}\n\t}\n\treturn alreadyNotifiedUserIDs\n}\n\nfunc (cs *ReviewService) notificationQuestionComment(ctx context.Context, questionUserID,\n\tquestionID, questionTitle, commentID, commentUserID, commentSummary string) {\n\tif questionUserID == commentUserID {\n\t\treturn\n\t}\n\t// send internal notification\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: questionUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationCommentQuestion\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// send external notification\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", questionUserID)\n\t\treturn\n\t}\n\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\nfunc (cs *ReviewService) notificationAnswerComment(ctx context.Context,\n\tquestionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {\n\tif answerUserID == commentUserID {\n\t\treturn\n\t}\n\n\t// Send internal notification.\n\tmsg := &schema.NotificationMsg{\n\t\tReceiverUserID: answerUserID,\n\t\tTriggerUserID:  commentUserID,\n\t\tType:           schema.NotificationTypeInbox,\n\t\tObjectID:       commentID,\n\t}\n\tmsg.ObjectType = constant.CommentObjectType\n\tmsg.NotificationAction = constant.NotificationCommentAnswer\n\tcs.notificationQueueService.Send(ctx, msg)\n\n\t// Send external notification.\n\treceiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !exist {\n\t\tlog.Warnf(\"user %s not found\", answerUserID)\n\t\treturn\n\t}\n\texternalNotificationMsg := &schema.ExternalNotificationMsg{\n\t\tReceiverUserID: receiverUserInfo.ID,\n\t\tReceiverEmail:  receiverUserInfo.EMail,\n\t\tReceiverLang:   receiverUserInfo.Language,\n\t}\n\trawData := &schema.NewCommentTemplateRawData{\n\t\tQuestionTitle:   questionTitle,\n\t\tQuestionID:      questionID,\n\t\tAnswerID:        answerID,\n\t\tCommentID:       commentID,\n\t\tCommentSummary:  commentSummary,\n\t\tUnsubscribeCode: token.GenerateToken(),\n\t}\n\tcommentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)\n\tif commentUser != nil {\n\t\trawData.CommentUserDisplayName = commentUser.DisplayName\n\t}\n\texternalNotificationMsg.NewCommentTemplateRawData = rawData\n\tcs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)\n}\n\n// GetReviewPendingCount get review pending count\nfunc (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) {\n\treturn cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending)\n}\n\n// GetUnreviewedPostPage get review page\nfunc (cs *ReviewService) GetUnreviewedPostPage(ctx context.Context, req *schema.GetUnreviewedPostPageReq) (\n\tpageModel *pager.PageModel, err error) {\n\tif !req.IsAdmin {\n\t\treturn pager.NewPageModel(0, make([]*schema.GetUnreviewedPostPageResp, 0)), nil\n\t}\n\tcond := &entity.Review{\n\t\tObjectID: req.ObjectID,\n\t\tStatus:   entity.ReviewStatusPending,\n\t}\n\treviewList, total, err := cs.reviewRepo.GetReviewPage(ctx, req.Page, 1, cond)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresp := make([]*schema.GetUnreviewedPostPageResp, 0)\n\tfor _, review := range reviewList {\n\t\tinfo, err := cs.objectInfoService.GetUnreviewedRevisionInfo(ctx, review.ObjectID)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"GetUnreviewedRevisionInfo failed, err: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tr := &schema.GetUnreviewedPostPageResp{\n\t\t\tReviewID:             review.ID,\n\t\t\tCreatedAt:            info.CreatedAt,\n\t\t\tObjectID:             info.ObjectID,\n\t\t\tQuestionID:           info.QuestionID,\n\t\t\tAnswerID:             info.AnswerID,\n\t\t\tCommentID:            info.CommentID,\n\t\t\tObjectType:           info.ObjectType,\n\t\t\tTitle:                info.Title,\n\t\t\tUrlTitle:             htmltext.UrlTitle(info.Title),\n\t\t\tOriginalText:         info.Content,\n\t\t\tParsedText:           info.Html,\n\t\t\tTags:                 info.Tags,\n\t\t\tObjectStatus:         info.Status,\n\t\t\tObjectShowStatus:     info.ShowStatus,\n\t\t\tSubmitAt:             review.CreatedAt.Unix(),\n\t\t\tSubmitterDisplayName: req.ReviewerMapping[review.Submitter],\n\t\t\tReason:               review.Reason,\n\t\t}\n\n\t\t// get user info\n\t\tuserInfo, exists, e := cs.userCommon.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID)\n\t\tif e != nil {\n\t\t\tlog.Errorf(\"user not found by id: %s, err: %v\", info.ObjectCreatorUserID, e)\n\t\t}\n\t\tif exists {\n\t\t\t_ = copier.Copy(&r.AuthorUserInfo, userInfo)\n\t\t}\n\t\tresp = append(resp, r)\n\t}\n\treturn pager.NewPageModel(total, resp), nil\n}\n"
  },
  {
    "path": "internal/service/revision/revision.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage revision\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"xorm.io/xorm\"\n)\n\n// RevisionRepo revision repository\ntype RevisionRepo interface {\n\tAddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error)\n\tGetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error)\n\tGetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)\n\tGetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error)\n\tGetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error)\n\tUpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error)\n\tExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)\n\tGetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error)\n\tCountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error)\n\tUpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error)\n}\n"
  },
  {
    "path": "internal/service/revision_common/revision_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage revision_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/revision\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/jinzhu/copier\"\n)\n\n// RevisionService user service\ntype RevisionService struct {\n\trevisionRepo revision.RevisionRepo\n\tuserRepo     usercommon.UserRepo\n}\n\nfunc NewRevisionService(revisionRepo revision.RevisionRepo,\n\tuserRepo usercommon.UserRepo,\n) *RevisionService {\n\treturn &RevisionService{\n\t\trevisionRepo: revisionRepo,\n\t\tuserRepo:     userRepo,\n\t}\n}\n\nfunc (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *schema.RevisionSearch) (count int64, err error) {\n\tif len(req.GetCanReviewObjectTypes()) == 0 {\n\t\treturn 0, nil\n\t}\n\treturn rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes())\n}\n\n// AddRevision add revision\n//\n// autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated,\n// if not need auto update object.revision_id, it must be false.\n// example: user can edit the object, but need audit, the revision_id will be updated when admin approved\nfunc (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (\n\trevisionID string, err error) {\n\treq.ObjectID = uid.DeShortID(req.ObjectID)\n\trev := &entity.Revision{}\n\t_ = copier.Copy(rev, req)\n\terr = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn rev.ID, nil\n}\n\n// GetRevision get revision\nfunc (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) (\n\trevision *entity.Revision, err error) {\n\trevisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.ObjectNotFound)\n\t}\n\treturn revisionInfo, nil\n}\n\n// ExistUnreviewedByObjectID\nfunc (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) {\n\tobjectID = uid.DeShortID(objectID)\n\trevision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID)\n\treturn revision, exist, err\n}\n"
  },
  {
    "path": "internal/service/role/power_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// PowerRepo power repository\ntype PowerRepo interface {\n\tGetPowerList(ctx context.Context, power *entity.Power) (powers []*entity.Power, err error)\n}\n"
  },
  {
    "path": "internal/service/role/role_power_rel_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n)\n\n// RolePowerRelRepo rolePowerRel repository\ntype RolePowerRelRepo interface {\n\tGetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error)\n}\n\n// RolePowerRelService user service\ntype RolePowerRelService struct {\n\trolePowerRelRepo   RolePowerRelRepo\n\tuserRoleRelService *UserRoleRelService\n}\n\n// NewRolePowerRelService new role power rel service\nfunc NewRolePowerRelService(rolePowerRelRepo RolePowerRelRepo,\n\tuserRoleRelService *UserRoleRelService) *RolePowerRelService {\n\treturn &RolePowerRelService{\n\t\trolePowerRelRepo:   rolePowerRelRepo,\n\t\tuserRoleRelService: userRoleRelService,\n\t}\n}\n\n// GetRolePowerList get role power list\nfunc (rs *RolePowerRelService) GetRolePowerList(ctx context.Context, roleID int) (powers []string, err error) {\n\treturn rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID)\n}\n\n// GetUserPowerList get  list all\nfunc (rs *RolePowerRelService) GetUserPowerList(ctx context.Context, userID string) (powers []string, err error) {\n\troleID, err := rs.userRoleRelService.GetUserRole(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID)\n}\n"
  },
  {
    "path": "internal/service/role/role_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/jinzhu/copier\"\n)\n\nconst (\n\t// Since there is currently no need to edit roles to add roles and other operations,\n\t// the current role information is translated directly.\n\t// Later on, when the relevant ability is available, it can be adjusted by the user himself.\n\n\tRoleUserID      = 1\n\tRoleAdminID     = 2\n\tRoleModeratorID = 3\n\n\troleUserName      = \"User\"\n\troleAdminName     = \"Admin\"\n\troleModeratorName = \"Moderator\"\n\n\ttrRoleNameUser      = \"role.name.user\"\n\ttrRoleNameAdmin     = \"role.name.admin\"\n\ttrRoleNameModerator = \"role.name.moderator\"\n\n\ttrRoleDescriptionUser      = \"role.description.user\"\n\ttrRoleDescriptionAdmin     = \"role.description.admin\"\n\ttrRoleDescriptionModerator = \"role.description.moderator\"\n)\n\n// RoleRepo role repository\ntype RoleRepo interface {\n\tGetRoleAllList(ctx context.Context) (roles []*entity.Role, err error)\n\tGetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error)\n}\n\n// RoleService user service\ntype RoleService struct {\n\troleRepo RoleRepo\n}\n\nfunc NewRoleService(roleRepo RoleRepo) *RoleService {\n\treturn &RoleService{\n\t\troleRepo: roleRepo,\n\t}\n}\n\n// GetRoleList get role list all\nfunc (rs *RoleService) GetRoleList(ctx context.Context) (resp []*schema.GetRoleResp, err error) {\n\troles, err := rs.roleRepo.GetRoleAllList(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, role := range roles {\n\t\trs.translateRole(ctx, role)\n\t}\n\n\tresp = []*schema.GetRoleResp{}\n\t_ = copier.Copy(&resp, roles)\n\treturn\n}\n\nfunc (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {\n\treturn rs.roleRepo.GetRoleAllMapping(ctx)\n}\n\nfunc (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) {\n\tswitch role.Name {\n\tcase roleUserName:\n\t\trole.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)\n\t\trole.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)\n\tcase roleAdminName:\n\t\trole.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)\n\t\trole.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)\n\tcase roleModeratorName:\n\t\trole.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)\n\t\trole.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)\n\t}\n}\n"
  },
  {
    "path": "internal/service/role/user_role_rel_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage role\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/entity\"\n)\n\n// UserRoleRelRepo userRoleRel repository\ntype UserRoleRelRepo interface {\n\tSaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error)\n\tGetUserRoleRelList(ctx context.Context, userIDs []string) (userRoleRelList []*entity.UserRoleRel, err error)\n\tGetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (\n\t\tuserRoleRelList []*entity.UserRoleRel, err error)\n\tGetUserRoleRel(ctx context.Context, userID string) (rolePowerRel *entity.UserRoleRel, exist bool, err error)\n}\n\n// UserRoleRelService user service\ntype UserRoleRelService struct {\n\tuserRoleRelRepo UserRoleRelRepo\n\troleService     *RoleService\n}\n\n// NewUserRoleRelService new user role rel service\nfunc NewUserRoleRelService(userRoleRelRepo UserRoleRelRepo, roleService *RoleService) *UserRoleRelService {\n\treturn &UserRoleRelService{\n\t\tuserRoleRelRepo: userRoleRelRepo,\n\t\troleService:     roleService,\n\t}\n}\n\n// SaveUserRole save user role\nfunc (us *UserRoleRelService) SaveUserRole(ctx context.Context, userID string, roleID int) (err error) {\n\treturn us.userRoleRelRepo.SaveUserRoleRel(ctx, userID, roleID)\n}\n\n// GetUserRoleMapping get user role mapping\nfunc (us *UserRoleRelService) GetUserRoleMapping(ctx context.Context, userIDs []string) (\n\tuserRoleMapping map[string]*entity.Role, err error) {\n\tuserRoleMapping = make(map[string]*entity.Role, 0)\n\troleMapping, err := us.roleService.GetRoleMapping(ctx)\n\tif err != nil {\n\t\treturn userRoleMapping, err\n\t}\n\tif len(roleMapping) == 0 {\n\t\treturn userRoleMapping, nil\n\t}\n\n\trelMapping, err := us.GetUserRoleRelMapping(ctx, userIDs)\n\tif err != nil {\n\t\treturn userRoleMapping, err\n\t}\n\n\t// default role is user\n\tdefaultRole := roleMapping[1]\n\tfor _, userID := range userIDs {\n\t\troleID, ok := relMapping[userID]\n\t\tif !ok {\n\t\t\tuserRoleMapping[userID] = defaultRole\n\t\t\tcontinue\n\t\t}\n\t\tuserRoleMapping[userID] = roleMapping[roleID]\n\t\tif userRoleMapping[userID] == nil {\n\t\t\tuserRoleMapping[userID] = defaultRole\n\t\t}\n\t}\n\treturn userRoleMapping, nil\n}\n\n// GetUserRoleRelMapping get user role rel mapping\nfunc (us *UserRoleRelService) GetUserRoleRelMapping(ctx context.Context, userIDs []string) (\n\tuserRoleRelMapping map[string]int, err error) {\n\tuserRoleRelMapping = make(map[string]int, 0)\n\n\trelList, err := us.userRoleRelRepo.GetUserRoleRelList(ctx, userIDs)\n\tif err != nil {\n\t\treturn userRoleRelMapping, err\n\t}\n\n\tfor _, rel := range relList {\n\t\tuserRoleRelMapping[rel.UserID] = rel.RoleID\n\t}\n\treturn userRoleRelMapping, nil\n}\n\n// GetUserRole get user role\nfunc (us *UserRoleRelService) GetUserRole(ctx context.Context, userID string) (roleID int, err error) {\n\trolePowerRel, exist, err := us.userRoleRelRepo.GetUserRoleRel(ctx, userID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif !exist {\n\t\t// set default role\n\t\treturn 1, nil\n\t}\n\treturn rolePowerRel.RoleID, nil\n}\n\n// GetUserByRoleID get user by role id\nfunc (us *UserRoleRelService) GetUserByRoleID(ctx context.Context, roleIDs []int) (rel []*entity.UserRoleRel, err error) {\n\trolePowerRels, err := us.userRoleRelRepo.GetUserRoleRelListByRoleID(ctx, roleIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rolePowerRels, nil\n}\n"
  },
  {
    "path": "internal/service/search_common/search.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage search_common\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/plugin\"\n)\n\ntype SearchRepo interface {\n\tSearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)\n\tSearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)\n\tSearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error)\n\tParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error)\n}\n"
  },
  {
    "path": "internal/service/search_parser/search_parser.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage search_parser\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/tag_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n)\n\ntype SearchParser struct {\n\ttagCommonService *tag_common.TagCommonService\n\tuserCommon       *usercommon.UserCommon\n}\n\nfunc NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser {\n\treturn &SearchParser{\n\t\ttagCommonService: tagCommonService,\n\t\tuserCommon:       userCommon,\n\t}\n}\n\n// ParseStructure parse search structure, maybe match one of type all/questions/answers,\n// but if match two type, it will return false\nfunc (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDTO) (cond *schema.SearchCondition) {\n\tcond = &schema.SearchCondition{}\n\tvar (\n\t\tquery      = dto.Query\n\t\tlimitWords = 5\n\t)\n\n\t// match tags\n\tcond.Tags = sp.parseTags(ctx, &query)\n\n\t// match all\n\tcond.UserID = sp.parseUserID(ctx, &query, dto.UserID)\n\tcond.VoteAmount = sp.parseVotes(&query)\n\tcond.Words = sp.parseWithin(&query)\n\n\t// match questions\n\tcond.NotAccepted = sp.parseNotAccepted(&query)\n\tif cond.NotAccepted {\n\t\tcond.TargetType = constant.QuestionObjectType\n\t}\n\tcond.Views = sp.parseViews(&query)\n\tif cond.Views != -1 {\n\t\tcond.TargetType = constant.QuestionObjectType\n\t}\n\tcond.AnswerAmount = sp.parseAnswers(&query)\n\tif cond.AnswerAmount != -1 {\n\t\tcond.TargetType = constant.QuestionObjectType\n\t}\n\n\t// match answers\n\tcond.Accepted = sp.parseAccepted(&query)\n\tif cond.Accepted {\n\t\tcond.TargetType = constant.AnswerObjectType\n\t}\n\tcond.QuestionID = sp.parseQuestionID(&query)\n\tif cond.QuestionID != \"\" {\n\t\tcond.TargetType = constant.AnswerObjectType\n\t}\n\n\tif sp.parseIsQuestion(&query) {\n\t\tcond.TargetType = constant.QuestionObjectType\n\t}\n\tif sp.parseIsAnswer(&query) {\n\t\tcond.TargetType = constant.AnswerObjectType\n\t}\n\n\tif len(strings.TrimSpace(query)) > 0 {\n\t\twords := strings.Split(strings.TrimSpace(query), \" \")\n\t\tcond.Words = append(cond.Words, words...)\n\t}\n\n\t// check limit words\n\tif len(cond.Words) > limitWords {\n\t\tcond.Words = cond.Words[:limitWords]\n\t}\n\treturn\n}\n\n// parseTags parse search tags, return tag ids array\nfunc (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][]string) {\n\tvar (\n\t\t// expire tag pattern\n\t\texprTag = `\\[(.*?)\\]`\n\t\tq       = *query\n\t\tlimit   = 5\n\t)\n\n\tre := regexp.MustCompile(exprTag)\n\tres := re.FindAllStringSubmatch(q, -1)\n\tif len(res) == 0 {\n\t\treturn\n\t}\n\n\ttags = make([][]string, 0)\n\tfor _, item := range res {\n\t\ttagGroup := make([]string, 0)\n\t\ttag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1])\n\t\tif err != nil || !exists {\n\t\t\tcontinue\n\t\t}\n\t\ttagGroup = append(tagGroup, tag.ID)\n\t\tif tag.MainTagID > 0 {\n\t\t\ttagGroup = append(tagGroup, fmt.Sprintf(\"%d\", tag.MainTagID))\n\t\t}\n\t\tsynIDs, err := sp.tagCommonService.GetTagIDsByMainTagID(ctx, tag.ID)\n\t\tif err != nil || !exists {\n\t\t\tcontinue\n\t\t}\n\t\ttagGroup = append(tagGroup, tag.ID)\n\t\ttagGroup = append(tagGroup, synIDs...)\n\t\ttagGroup = converter.UniqueArray(tagGroup)\n\t\ttags = append(tags, tagGroup)\n\t}\n\n\t// limit maximum 5 tags\n\tif len(tags) > limit {\n\t\ttags = tags[:limit]\n\t}\n\n\tq = strings.TrimSpace(re.ReplaceAllString(q, \"\"))\n\t*query = q\n\treturn\n}\n\n// parseUserID return user id or current login user id\nfunc (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) {\n\tvar (\n\t\texprUsername = `user:(\\S+)`\n\t\texprMe       = \"user:me\"\n\t\tq            = *query\n\t)\n\n\tre := regexp.MustCompile(exprUsername)\n\tres := re.FindStringSubmatch(q)\n\tif strings.Contains(q, exprMe) {\n\t\tuserID = currentUserID\n\t\tq = strings.ReplaceAll(q, exprMe, \"\")\n\t} else if len(res) > 1 {\n\t\tname := res[1]\n\t\tuser, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name)\n\t\tif err == nil && has {\n\t\t\tuserID = user.ID\n\t\t\tq = re.ReplaceAllString(q, \"\")\n\t\t}\n\t}\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseVotes return the votes of search query\nfunc (sp *SearchParser) parseVotes(query *string) (votes int) {\n\tvar (\n\t\texpr = `score:(\\d+)`\n\t\tq    = *query\n\t)\n\tvotes = -1\n\n\tre := regexp.MustCompile(expr)\n\tres := re.FindStringSubmatch(q)\n\tif len(res) > 1 {\n\t\tvotes = converter.StringToInt(res[1])\n\t\tq = re.ReplaceAllString(q, \"\")\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseWithin parse quotes within words like: \"hello world\"\nfunc (sp *SearchParser) parseWithin(query *string) (words []string) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `(?U)(\".+\")`\n\t)\n\tre := regexp.MustCompile(expr)\n\tmatches := re.FindAllStringSubmatch(q, -1)\n\twords = []string{}\n\tfor _, match := range matches {\n\t\tif len(match[1]) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\twords = append(words, match[1])\n\t}\n\tq = re.ReplaceAllString(q, \"\")\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseNotAccepted return the question has not accepted the answer\nfunc (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `hasaccepted:no`\n\t)\n\n\tif strings.Contains(q, expr) {\n\t\tq = strings.ReplaceAll(q, expr, \"\")\n\t\tnotAccepted = true\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseIsQuestion check the result if only limit question or not\nfunc (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `is:question`\n\t)\n\n\tif strings.Contains(q, expr) {\n\t\tq = strings.ReplaceAll(q, expr, \"\")\n\t\tisQuestion = true\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseViews check search has views or not\nfunc (sp *SearchParser) parseViews(query *string) (views int) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `views:(\\d+)`\n\t)\n\tviews = -1\n\n\tre := regexp.MustCompile(expr)\n\tres := re.FindStringSubmatch(q)\n\tif len(res) > 1 {\n\t\tviews = converter.StringToInt(res[1])\n\t\tq = re.ReplaceAllString(q, \"\")\n\t}\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseAnswers check whether specified answer count for question\nfunc (sp *SearchParser) parseAnswers(query *string) (answers int) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `answers:(\\d+)`\n\t)\n\tanswers = -1\n\n\tre := regexp.MustCompile(expr)\n\tres := re.FindStringSubmatch(q)\n\tif len(res) > 1 {\n\t\tanswers = converter.StringToInt(res[1])\n\t\tq = re.ReplaceAllString(q, \"\")\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseAccepted check the search is limit accepted answer or not\nfunc (sp *SearchParser) parseAccepted(query *string) (accepted bool) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `isaccepted:yes`\n\t)\n\n\tif strings.Contains(q, expr) {\n\t\taccepted = true\n\t\tq = strings.ReplaceAll(q, expr, \"\")\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseQuestionID check whether specified question's id\nfunc (sp *SearchParser) parseQuestionID(query *string) (questionID string) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `inquestion:(\\d+)`\n\t)\n\n\tre := regexp.MustCompile(expr)\n\tres := re.FindStringSubmatch(q)\n\tif len(res) == 2 {\n\t\tquestionID = res[1]\n\t\tq = re.ReplaceAllString(q, \"\")\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n\n// parseIsAnswer check the result if only limit answer or not\nfunc (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {\n\tvar (\n\t\tq    = *query\n\t\texpr = `is:answer`\n\t)\n\n\tif strings.Contains(q, expr) {\n\t\tisAnswer = true\n\t\tq = strings.ReplaceAll(q, expr, \"\")\n\t}\n\n\t*query = strings.TrimSpace(q)\n\treturn\n}\n"
  },
  {
    "path": "internal/service/service_config/service_config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage service_config\n\ntype ServiceConfig struct {\n\tUploadPath                    string `json:\"upload_path\" mapstructure:\"upload_path\" yaml:\"upload_path\"`\n\tCleanUpUploads                bool   `json:\"clean_up_uploads\" mapstructure:\"clean_up_uploads\" yaml:\"clean_up_uploads\"`\n\tCleanOrphanUploadsPeriodHours int    `json:\"clean_orphan_uploads_period_hours\" mapstructure:\"clean_orphan_uploads_period_hours\" yaml:\"clean_orphan_uploads_period_hours\"`\n\tPurgeDeletedFilesPeriodDays   int    `json:\"purge_deleted_files_period_days\" mapstructure:\"purge_deleted_files_period_days\" yaml:\"purge_deleted_files_period_days\"`\n}\n"
  },
  {
    "path": "internal/service/siteinfo/siteinfo_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage siteinfo\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\terrpkg \"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/config\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/file_record\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttagcommon \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/go-resty/resty/v2\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype SiteInfoService struct {\n\tsiteInfoRepo          siteinfo_common.SiteInfoRepo\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n\temailService          *export.EmailService\n\ttagCommonService      *tagcommon.TagCommonService\n\tconfigService         *config.ConfigService\n\tquestioncommon        *questioncommon.QuestionCommon\n\tfileRecordService     *file_record.FileRecordService\n}\n\nfunc NewSiteInfoService(\n\tsiteInfoRepo siteinfo_common.SiteInfoRepo,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n\temailService *export.EmailService,\n\ttagCommonService *tagcommon.TagCommonService,\n\tconfigService *config.ConfigService,\n\tquestioncommon *questioncommon.QuestionCommon,\n\tfileRecordService *file_record.FileRecordService,\n\n) *SiteInfoService {\n\tplugin.RegisterGetSiteURLFunc(func() string {\n\t\tgeneralSiteInfo, err := siteInfoCommonService.GetSiteGeneral(context.Background())\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn \"\"\n\t\t}\n\t\treturn generalSiteInfo.SiteUrl\n\t})\n\n\treturn &SiteInfoService{\n\t\tsiteInfoRepo:          siteInfoRepo,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t\temailService:          emailService,\n\t\ttagCommonService:      tagCommonService,\n\t\tconfigService:         configService,\n\t\tquestioncommon:        questioncommon,\n\t\tfileRecordService:     fileRecordService,\n\t}\n}\n\n// GetSiteGeneral get site info general\nfunc (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteGeneral(ctx)\n}\n\n// GetSiteInterface get site info interface\nfunc (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteInterface(ctx)\n}\n\n// GetSiteUsersSettings get site info users settings\nfunc (s *SiteInfoService) GetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteUsersSettings(ctx)\n}\n\n// GetSiteBranding get site info branding\nfunc (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteBranding(ctx)\n}\n\n// GetSiteUsers get site info about users\nfunc (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteUsers(ctx)\n}\n\n// GetSiteTag get site info write\nfunc (s *SiteInfoService) GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error) {\n\tresp, err = s.siteInfoCommonService.GetSiteTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp, nil\n\t}\n\n\tresp.RecommendTags, err = s.tagCommonService.GetSiteWriteRecommendTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tresp.ReservedTags, err = s.tagCommonService.GetSiteWriteReservedTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn resp, nil\n}\n\n// GetSiteQuestion get site questions settings\nfunc (s *SiteInfoService) GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteQuestion(ctx)\n}\n\n// GetSiteAdvanced get site advanced settings\nfunc (s *SiteInfoService) GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteAdvanced(ctx)\n}\n\n// GetSitePolicies get site legal info\nfunc (s *SiteInfoService) GetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error) {\n\treturn s.siteInfoCommonService.GetSitePolicies(ctx)\n}\n\n// GetSiteSecurity get site security info\nfunc (s *SiteInfoService) GetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteSecurity(ctx)\n}\n\n// GetSiteLogin get site login info\nfunc (s *SiteInfoService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteLogin(ctx)\n}\n\n// GetSiteCustomCssHTML get site custom css html config\nfunc (s *SiteInfoService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteCustomCssHTML(ctx)\n}\n\n// GetSiteTheme get site theme config\nfunc (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {\n\treturn s.siteInfoCommonService.GetSiteTheme(ctx)\n}\n\nfunc (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {\n\treq.FormatSiteUrl()\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeGeneral,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data)\n}\n\nfunc (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {\n\t// If the language is invalid, set it to the default language \"en_US\"\n\tif !translator.CheckLanguageIsValid(req.Language) {\n\t\treq.Language = \"en_US\"\n\t}\n\n\tcontent, _ := json.Marshal(req)\n\tdata := entity.SiteInfo{\n\t\tType:    constant.SiteTypeInterfaceSettings,\n\t\tContent: string(content),\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterfaceSettings, &data)\n}\n\n// SaveSiteUsersSettings save site users settings\nfunc (s *SiteInfoService) SaveSiteUsersSettings(ctx context.Context, req schema.SiteUsersSettingsReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := entity.SiteInfo{\n\t\tType:    constant.SiteTypeInterfaceSettings,\n\t\tContent: string(content),\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsersSettings, &data)\n}\n\n// SaveSiteBranding save site branding information\nfunc (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.SiteBrandingReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeBranding,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data)\n}\n\n// SaveSiteAdvanced save site advanced configuration\nfunc (s *SiteInfoService) SaveSiteAdvanced(ctx context.Context, req *schema.SiteAdvancedReq) (resp any, err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeAdvanced,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeAdvanced, data)\n}\n\n// SaveSiteQuestions save site questions configuration\nfunc (s *SiteInfoService) SaveSiteQuestions(ctx context.Context, req *schema.SiteQuestionsReq) (resp any, err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeQuestions,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeQuestions, data)\n}\n\n// SaveSiteTags save site tags configuration\nfunc (s *SiteInfoService) SaveSiteTags(ctx context.Context, req *schema.SiteTagsReq) (resp any, err error) {\n\trecommendTags, reservedTags := make([]string, 0), make([]string, 0)\n\trecommendTagMapping, reservedTagMapping := make(map[string]bool), make(map[string]bool)\n\tfor _, tag := range req.ReservedTags {\n\t\tif !recommendTagMapping[tag.SlugName] {\n\t\t\treservedTagMapping[tag.SlugName] = true\n\t\t\treservedTags = append(reservedTags, tag.SlugName)\n\t\t}\n\t}\n\n\t// recommend tags can't contain reserved tag\n\tfor _, tag := range req.RecommendTags {\n\t\tif reservedTagMapping[tag.SlugName] {\n\t\t\tcontinue\n\t\t}\n\t\tif !recommendTagMapping[tag.SlugName] {\n\t\t\trecommendTagMapping[tag.SlugName] = true\n\t\t\trecommendTags = append(recommendTags, tag.SlugName)\n\t\t}\n\t}\n\terrData, err := s.tagCommonService.SetSiteWriteTag(ctx, recommendTags, reservedTags, req.UserID)\n\tif err != nil {\n\t\treturn errData, err\n\t}\n\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeTags,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTags, data)\n}\n\n// SaveSitePolicies save site policies configuration\nfunc (s *SiteInfoService) SaveSitePolicies(ctx context.Context, req *schema.SitePoliciesReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypePolicies,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePolicies, data)\n}\n\n// SaveSiteSecurity save site security configuration\nfunc (s *SiteInfoService) SaveSiteSecurity(ctx context.Context, req *schema.SiteSecurityReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeSecurity,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSecurity, data)\n}\n\n// SaveSiteLogin save site legal configuration\nfunc (s *SiteInfoService) SaveSiteLogin(ctx context.Context, req *schema.SiteLoginReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeLogin,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLogin, data)\n}\n\n// SaveSiteCustomCssHTML save site custom html configuration\nfunc (s *SiteInfoService) SaveSiteCustomCssHTML(ctx context.Context, req *schema.SiteCustomCssHTMLReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeCustomCssHTML,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeCustomCssHTML, data)\n}\n\n// SaveSiteTheme save site custom html configuration\nfunc (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThemeReq) (err error) {\n\tif len(req.Layout) == 0 {\n\t\treq.Layout = constant.ThemeLayoutFullWidth\n\t}\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeTheme,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data)\n}\n\n// SaveSiteUsers save site users\nfunc (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeUsers,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)\n}\n\n// GetSiteAI get site AI configuration\nfunc (s *SiteInfoService) GetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error) {\n\tresp, err = s.siteInfoCommonService.GetSiteAI(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taiProvider, err := s.GetAIProvider(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tproviderMapping := make(map[string]*schema.SiteAIProvider)\n\tfor _, provider := range resp.SiteAIProviders {\n\t\tproviderMapping[provider.Provider] = provider\n\t}\n\tproviders := make([]*schema.SiteAIProvider, 0)\n\tfor _, p := range aiProvider {\n\t\tif provider, ok := providerMapping[p.Name]; ok {\n\t\t\tproviders = append(providers, provider)\n\t\t} else {\n\t\t\tproviders = append(providers, &schema.SiteAIProvider{\n\t\t\t\tProvider: p.Name,\n\t\t\t})\n\t\t}\n\t}\n\tresp.SiteAIProviders = providers\n\ts.maskAIKeys(resp)\n\treturn resp, nil\n}\n\n// SaveSiteAI save site AI configuration\nfunc (s *SiteInfoService) SaveSiteAI(ctx context.Context, req *schema.SiteAIReq) (err error) {\n\tif err := s.restoreMaskedAIKeys(ctx, req); err != nil {\n\t\treturn err\n\t}\n\tif req.PromptConfig == nil {\n\t\treq.PromptConfig = &schema.AIPromptConfig{\n\t\t\tZhCN: constant.DefaultAIPromptConfigZhCN,\n\t\t\tEnUS: constant.DefaultAIPromptConfigEnUS,\n\t\t}\n\t}\n\n\taiProvider, err := s.GetAIProvider(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tproviderMapping := make(map[string]*schema.SiteAIProvider)\n\tfor _, provider := range req.SiteAIProviders {\n\t\tproviderMapping[provider.Provider] = provider\n\t}\n\n\tproviders := make([]*schema.SiteAIProvider, 0)\n\tfor _, p := range aiProvider {\n\t\tif provider, ok := providerMapping[p.Name]; ok {\n\t\t\tif len(provider.APIHost) == 0 && provider.Provider == req.ChosenProvider {\n\t\t\t\tprovider.APIHost = p.DefaultAPIHost\n\t\t\t}\n\t\t\tproviders = append(providers, provider)\n\t\t} else {\n\t\t\tproviders = append(providers, &schema.SiteAIProvider{\n\t\t\t\tProvider: p.Name,\n\t\t\t\tAPIHost:  p.DefaultAPIHost,\n\t\t\t})\n\t\t}\n\t}\n\treq.SiteAIProviders = providers\n\n\tcontent, _ := json.Marshal(req)\n\tsiteInfo := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeAI,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeAI, siteInfo)\n}\n\nfunc (s *SiteInfoService) maskAIKeys(resp *schema.SiteAIResp) {\n\tfor _, provider := range resp.SiteAIProviders {\n\t\tif provider.APIKey == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tprovider.APIKey = strings.Repeat(\"*\", len(provider.APIKey))\n\t}\n}\n\nfunc (s *SiteInfoService) restoreMaskedAIKeys(ctx context.Context, req *schema.SiteAIReq) error {\n\thasMasked := false\n\tfor _, provider := range req.SiteAIProviders {\n\t\tif provider.APIKey != \"\" && isAllMask(provider.APIKey) {\n\t\t\thasMasked = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !hasMasked {\n\t\treturn nil\n\t}\n\n\tcurrent, err := s.siteInfoCommonService.GetSiteAI(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrentMapping := make(map[string]*schema.SiteAIProvider)\n\tfor _, provider := range current.SiteAIProviders {\n\t\tcurrentMapping[provider.Provider] = provider\n\t}\n\tfor _, provider := range req.SiteAIProviders {\n\t\tif provider.APIKey == \"\" || !isAllMask(provider.APIKey) {\n\t\t\tcontinue\n\t\t}\n\t\tif stored, ok := currentMapping[provider.Provider]; ok {\n\t\t\tprovider.APIKey = stored.APIKey\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc isAllMask(value string) bool {\n\treturn strings.Trim(value, \"*\") == \"\"\n}\n\n// GetSiteMCP get site MCP configuration\nfunc (s *SiteInfoService) GetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error) {\n\tresp, err = s.siteInfoCommonService.GetSiteMCP(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsiteInfo, err := s.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.Type = \"Server-Sent Event (SSE)\"\n\tresp.URL = fmt.Sprintf(\"%s/answer/api/v1/mcp/sse\", siteInfo.SiteUrl)\n\tresp.HTTPHeader = \"Authorization={key}\"\n\treturn\n}\n\n// SaveSiteMCP save site MCP configuration\nfunc (s *SiteInfoService) SaveSiteMCP(ctx context.Context, req *schema.SiteMCPReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tsiteInfo := &entity.SiteInfo{\n\t\tType:    constant.SiteTypeMCP,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeMCP, siteInfo)\n}\n\n// GetSMTPConfig get smtp config\nfunc (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (resp *schema.GetSMTPConfigResp, err error) {\n\temailConfig, err := s.emailService.GetEmailConfig(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &schema.GetSMTPConfigResp{}\n\t_ = copier.Copy(resp, emailConfig)\n\tresp.SMTPPassword = strings.Repeat(\"*\", len(resp.SMTPPassword))\n\treturn resp, nil\n}\n\n// UpdateSMTPConfig get smtp config\nfunc (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.UpdateSMTPConfigReq) (err error) {\n\temailConfig, err := s.emailService.GetEmailConfig(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tec := &export.EmailConfig{}\n\t_ = copier.Copy(ec, req)\n\n\tif len(ec.SMTPPassword) > 0 && ec.SMTPPassword == strings.Repeat(\"*\", len(ec.SMTPPassword)) {\n\t\tec.SMTPPassword = emailConfig.SMTPPassword\n\t}\n\n\terr = s.emailService.SetEmailConfig(ctx, ec)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(req.TestEmailRecipient) > 0 {\n\t\ttitle, body, err := s.emailService.TestTemplate(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo s.emailService.Send(ctx, req.TestEmailRecipient, title, body)\n\t}\n\treturn nil\n}\n\nfunc (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {\n\tresp = &schema.SiteSeoReq{}\n\tif err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {\n\t\treturn resp, err\n\t}\n\tsiteSecurity, err := s.GetSiteSecurity(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp, nil\n\t}\n\t// If the site is set to privacy mode, prohibit crawling any page.\n\tif siteSecurity.LoginRequired {\n\t\tresp.Robots = \"User-agent: *\\nDisallow: /\"\n\t\treturn resp, nil\n\t}\n\treturn resp, nil\n}\n\nfunc (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) {\n\tcontent, _ := json.Marshal(req)\n\tdata := entity.SiteInfo{\n\t\tType:    constant.SiteTypeSeo,\n\t\tContent: string(content),\n\t}\n\treturn s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data)\n}\n\nfunc (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) {\n\tprivilege := &schema.UpdatePrivilegesConfigReq{}\n\tif err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil {\n\t\treturn nil, err\n\t}\n\tprivilegeOptions := schema.DefaultPrivilegeOptions\n\tif len(privilege.CustomPrivileges) > 0 {\n\t\tprivilegeOptions = append(privilegeOptions, &schema.PrivilegeOption{\n\t\t\tLevel:      schema.PrivilegeLevelCustom,\n\t\t\tLevelDesc:  reason.PrivilegeLevelCustomDesc,\n\t\t\tPrivileges: privilege.CustomPrivileges,\n\t\t})\n\t} else {\n\t\tprivilegeOptions = append(privilegeOptions, schema.DefaultCustomPrivilegeOption)\n\t}\n\tresp = &schema.GetPrivilegesConfigResp{\n\t\tOptions:       s.translatePrivilegeOptions(ctx, privilegeOptions),\n\t\tSelectedLevel: schema.PrivilegeLevel3,\n\t}\n\tif privilege.Level > 0 {\n\t\tresp.SelectedLevel = privilege.Level\n\t}\n\treturn resp, nil\n}\n\nfunc (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context, privilegeOptions []*schema.PrivilegeOption) (options []*schema.PrivilegeOption) {\n\tla := handler.GetLangByCtx(ctx)\n\tfor _, option := range privilegeOptions {\n\t\top := &schema.PrivilegeOption{\n\t\t\tLevel:     option.Level,\n\t\t\tLevelDesc: translator.Tr(la, option.LevelDesc),\n\t\t}\n\t\tfor _, privilege := range option.Privileges {\n\t\t\top.Privileges = append(op.Privileges, &constant.Privilege{\n\t\t\t\tKey:   privilege.Key,\n\t\t\t\tLabel: translator.Tr(la, privilege.Label),\n\t\t\t\tValue: privilege.Value,\n\t\t\t})\n\t\t}\n\t\toptions = append(options, op)\n\t}\n\treturn\n}\n\nfunc (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) {\n\tvar choosePrivileges []*constant.Privilege\n\tif req.Level == schema.PrivilegeLevelCustom {\n\t\tchoosePrivileges = req.CustomPrivileges\n\t} else {\n\t\tchooseOption := schema.DefaultPrivilegeOptions.Choose(req.Level)\n\t\tif chooseOption == nil {\n\t\t\treturn nil\n\t\t}\n\t\tchoosePrivileges = chooseOption.Privileges\n\t}\n\tif choosePrivileges == nil {\n\t\treturn nil\n\t}\n\n\t// update site info that user choose which privilege level\n\tif req.Level == schema.PrivilegeLevelCustom {\n\t\tprivilegeMap := make(map[string]int)\n\t\tfor _, privilege := range req.CustomPrivileges {\n\t\t\tprivilegeMap[privilege.Key] = privilege.Value\n\t\t}\n\t\tvar privileges []*constant.Privilege\n\t\tfor _, privilege := range constant.RankAllPrivileges {\n\t\t\tprivileges = append(privileges, &constant.Privilege{\n\t\t\t\tKey:   privilege.Key,\n\t\t\t\tLabel: privilege.Label,\n\t\t\t\tValue: privilegeMap[privilege.Key],\n\t\t\t})\n\t\t}\n\t\treq.CustomPrivileges = privileges\n\t} else {\n\t\tprivilege := &schema.UpdatePrivilegesConfigReq{}\n\t\tif err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.CustomPrivileges = privilege.CustomPrivileges\n\t}\n\n\tcontent, _ := json.Marshal(req)\n\tdata := &entity.SiteInfo{\n\t\tType:    constant.SiteTypePrivileges,\n\t\tContent: string(content),\n\t\tStatus:  1,\n\t}\n\terr = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// update privilege in config\n\tfor _, privilege := range choosePrivileges {\n\t\terr = s.configService.UpdateConfig(ctx, privilege.Key, fmt.Sprintf(\"%d\", privilege.Value))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn\n}\n\nfunc (s *SiteInfoService) CleanUpRemovedBrandingFiles(\n\tctx context.Context,\n\tnewBranding *schema.SiteBrandingReq,\n\tcurrentBranding *schema.SiteBrandingResp,\n) error {\n\tvar allErrors []error\n\tcurrentFiles := map[string]string{\n\t\t\"logo\":        currentBranding.Logo,\n\t\t\"mobile_logo\": currentBranding.MobileLogo,\n\t\t\"square_icon\": currentBranding.SquareIcon,\n\t\t\"favicon\":     currentBranding.Favicon,\n\t}\n\n\tnewFiles := map[string]string{\n\t\t\"logo\":        newBranding.Logo,\n\t\t\"mobile_logo\": newBranding.MobileLogo,\n\t\t\"square_icon\": newBranding.SquareIcon,\n\t\t\"favicon\":     newBranding.Favicon,\n\t}\n\n\tfor key, currentFile := range currentFiles {\n\t\tnewFile := newFiles[key]\n\t\tif currentFile != \"\" && currentFile != newFile {\n\t\t\tfileRecord, err := s.fileRecordService.GetFileRecordByURL(ctx, currentFile)\n\t\t\tif err != nil {\n\t\t\t\tallErrors = append(allErrors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif fileRecord == nil {\n\t\t\t\terr := errpkg.New(\"file record is nil for key \" + key)\n\t\t\t\tallErrors = append(allErrors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := s.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil {\n\t\t\t\tallErrors = append(allErrors, err)\n\t\t\t}\n\t\t}\n\t}\n\tif len(allErrors) > 0 {\n\t\treturn errpkg.Join(allErrors...)\n\t}\n\treturn nil\n}\n\nfunc (s *SiteInfoService) GetAIProvider(ctx context.Context) (resp []*schema.GetAIProviderResp, err error) {\n\tresp = make([]*schema.GetAIProviderResp, 0)\n\taiProviderConfig, err := s.configService.GetStringValue(context.TODO(), constant.AIConfigProvider)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp, nil\n\t}\n\n\t_ = json.Unmarshal([]byte(aiProviderConfig), &resp)\n\treturn resp, nil\n}\n\nfunc (s *SiteInfoService) GetAIModels(ctx context.Context, req *schema.GetAIModelsReq) (resp []*schema.GetAIModelResp, err error) {\n\tresp = make([]*schema.GetAIModelResp, 0)\n\tif req.APIKey != \"\" && isAllMask(req.APIKey) {\n\t\tstoredKey, err := s.getStoredAIKey(ctx, req.APIHost)\n\t\tif err != nil {\n\t\t\treturn resp, err\n\t\t}\n\t\tif storedKey == \"\" {\n\t\t\treturn resp, errors.BadRequest(\"api_key is required\")\n\t\t}\n\t\treq.APIKey = storedKey\n\t}\n\n\tr := resty.New()\n\tr.SetHeader(\"Authorization\", fmt.Sprintf(\"Bearer %s\", req.APIKey))\n\tr.SetHeader(\"Content-Type\", \"application/json\")\n\trespBody, err := r.R().Get(req.APIHost + \"/v1/models\")\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp, errors.BadRequest(fmt.Sprintf(\"failed to get AI models %s\", err.Error()))\n\t}\n\tif !respBody.IsSuccess() {\n\t\tlog.Error(fmt.Sprintf(\"failed to get AI models, status code: %d, body: %s\", respBody.StatusCode(), respBody.String()))\n\t\treturn resp, errors.BadRequest(fmt.Sprintf(\"failed to get AI models, response: %s\", respBody.String()))\n\t}\n\n\tdata := schema.GetAIModelsResp{}\n\t_ = json.Unmarshal(respBody.Body(), &data)\n\n\tfor _, model := range data.Data {\n\t\tresp = append(resp, &schema.GetAIModelResp{\n\t\t\tId:      model.Id,\n\t\t\tObject:  model.Object,\n\t\t\tCreated: model.Created,\n\t\t\tOwnedBy: model.OwnedBy,\n\t\t})\n\t}\n\treturn resp, nil\n}\n\nfunc (s *SiteInfoService) getStoredAIKey(ctx context.Context, apiHost string) (string, error) {\n\tcurrent, err := s.siteInfoCommonService.GetSiteAI(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tapiHost = strings.TrimRight(apiHost, \"/\")\n\tfor _, provider := range current.SiteAIProviders {\n\t\tif strings.TrimRight(provider.APIHost, \"/\") == apiHost && provider.APIKey != \"\" {\n\t\t\treturn provider.APIKey, nil\n\t\t}\n\t}\n\tif current.ChosenProvider != \"\" {\n\t\tfor _, provider := range current.SiteAIProviders {\n\t\t\tif provider.Provider == current.ChosenProvider {\n\t\t\t\treturn provider.APIKey, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "internal/service/siteinfo_common/siteinfo_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage siteinfo_common\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"html\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/pkg/gravatar\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n//go:generate mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock\ntype SiteInfoRepo interface {\n\tSaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error)\n\tGetByType(ctx context.Context, siteType string, withoutCache ...bool) (siteInfo *entity.SiteInfo, exist bool, err error)\n\tIsBrandingFileUsed(ctx context.Context, filePath string) (bool, error)\n}\n\n// siteInfoCommonService site info common service\ntype siteInfoCommonService struct {\n\tsiteInfoRepo SiteInfoRepo\n}\n\ntype SiteInfoCommonService interface {\n\tGetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error)\n\tGetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error)\n\tGetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error)\n\tGetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error)\n\tGetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error)\n\tFormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo\n\tFormatListAvatar(ctx context.Context, userList []*entity.User) (userID2AvatarMapping map[string]*schema.AvatarInfo)\n\tGetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error)\n\tGetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error)\n\tGetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error)\n\tGetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error)\n\tGetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error)\n\tGetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error)\n\tGetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error)\n\tGetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error)\n\tGetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error)\n\tGetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error)\n\tGetSiteInfoByType(ctx context.Context, siteType string, resp any) (err error)\n\tIsBrandingFileUsed(ctx context.Context, filePath string) bool\n\tGetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error)\n\tGetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error)\n}\n\n// NewSiteInfoCommonService new site info common service\nfunc NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService {\n\treturn &siteInfoCommonService{\n\t\tsiteInfoRepo: siteInfoRepo,\n\t}\n}\n\n// GetSiteGeneral get site info general\nfunc (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {\n\tresp = &schema.SiteGeneralResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Name = html.UnescapeString(resp.Name)\n\treturn resp, nil\n}\n\n// GetSiteInterface get site info interface\nfunc (s *siteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceSettingsResp, err error) {\n\tresp = &schema.SiteInterfaceSettingsResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterfaceSettings, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteUsersSettings get site info interface\nfunc (s *siteInfoCommonService) GetSiteUsersSettings(ctx context.Context) (resp *schema.SiteUsersSettingsResp, err error) {\n\tresp = &schema.SiteUsersSettingsResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsersSettings, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteBranding get site info branding\nfunc (s *siteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {\n\tresp = &schema.SiteBrandingResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteUsers get site info about users\nfunc (s *siteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {\n\tresp = &schema.SiteUsersResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// FormatAvatar format avatar\nfunc (s *siteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo {\n\tgravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)\n\treturn s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email, userStatus)\n}\n\n// FormatListAvatar format avatar\nfunc (s *siteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) (\n\tavatarMapping map[string]*schema.AvatarInfo) {\n\tgravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)\n\tavatarMapping = make(map[string]*schema.AvatarInfo)\n\tfor _, user := range userList {\n\t\tavatarMapping[user.ID] = s.selectedAvatar(user.Avatar, defaultAvatar, gravatarBaseURL, user.EMail, user.Status)\n\t}\n\treturn avatarMapping\n}\n\nfunc (s *siteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) {\n\tgravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar\n\tusersConfig, err := s.GetSiteUsersSettings(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tif len(usersConfig.GravatarBaseURL) > 0 {\n\t\tgravatarBaseURL = usersConfig.GravatarBaseURL\n\t}\n\tif len(usersConfig.DefaultAvatar) > 0 {\n\t\tdefaultAvatar = usersConfig.DefaultAvatar\n\t}\n\treturn gravatarBaseURL, defaultAvatar\n}\n\nfunc (s *siteInfoCommonService) selectedAvatar(\n\toriginalAvatarData,\n\tdefaultAvatar, gravatarBaseURL,\n\temail string, userStatus int) *schema.AvatarInfo {\n\tavatarInfo := &schema.AvatarInfo{}\n\t_ = json.Unmarshal([]byte(originalAvatarData), avatarInfo)\n\n\tif userStatus == entity.UserStatusDeleted {\n\t\treturn &schema.AvatarInfo{\n\t\t\tType: constant.DefaultAvatar,\n\t\t}\n\t}\n\n\tif len(avatarInfo.Type) == 0 && defaultAvatar == constant.AvatarTypeGravatar {\n\t\tavatarInfo.Type = constant.AvatarTypeGravatar\n\t\tavatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email)\n\t} else if avatarInfo.Type == constant.AvatarTypeGravatar {\n\t\tavatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email)\n\t}\n\treturn avatarInfo\n}\n\n// GetSiteWrite get site info write\nfunc (s *siteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {\n\tresp = &schema.SiteWriteResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteAdvanced get site info advanced\nfunc (s *siteInfoCommonService) GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, err error) {\n\tresp = &schema.SiteAdvancedResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeAdvanced, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteQuestion get site info question\nfunc (s *siteInfoCommonService) GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, err error) {\n\tresp = &schema.SiteQuestionsResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeQuestions, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteTag get site info tag\nfunc (s *siteInfoCommonService) GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error) {\n\tresp = &schema.SiteTagsResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeTags, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSitePolicies get site info policies\nfunc (s *siteInfoCommonService) GetSitePolicies(ctx context.Context) (resp *schema.SitePoliciesResp, err error) {\n\tresp = &schema.SitePoliciesResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypePolicies, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteSecurity get site security config\nfunc (s *siteInfoCommonService) GetSiteSecurity(ctx context.Context) (resp *schema.SiteSecurityResp, err error) {\n\tresp = &schema.SiteSecurityResp{CheckUpdate: true}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeSecurity, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteLogin get site login config\nfunc (s *siteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {\n\tresp = &schema.SiteLoginResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteCustomCssHTML get site custom css html config\nfunc (s *siteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {\n\tresp = &schema.SiteCustomCssHTMLResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteTheme get site theme\nfunc (s *siteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {\n\tresp = &schema.SiteThemeResp{\n\t\tThemeOptions: schema.GetThemeOptions,\n\t\tLayout:       constant.ThemeLayoutFullWidth,\n\t}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Layout == \"\" {\n\t\tresp.Layout = constant.ThemeLayoutFullWidth\n\t}\n\tresp.TrTheme(ctx)\n\treturn resp, nil\n}\n\n// GetSiteSeo get site seo\nfunc (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {\n\tresp = &schema.SiteSeoResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (s *siteInfoCommonService) EnableShortID(ctx context.Context) (enabled bool) {\n\tsiteSeo, err := s.GetSiteSeo(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn false\n\t}\n\treturn siteSeo.IsShortLink()\n}\n\nfunc (s *siteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) (err error) {\n\tsiteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn nil\n\t}\n\t_ = json.Unmarshal([]byte(siteInfo.Content), resp)\n\treturn nil\n}\n\nfunc (s *siteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool {\n\tused, err := s.siteInfoRepo.IsBrandingFileUsed(ctx, filePath)\n\tif err != nil {\n\t\tlog.Errorf(\"error checking if branding file is used: %v\", err)\n\t\t// will try again with the next clean up\n\t\treturn true\n\t}\n\treturn used\n}\n\n// GetSiteAI get site AI configuration\nfunc (s *siteInfoCommonService) GetSiteAI(ctx context.Context) (resp *schema.SiteAIResp, err error) {\n\tresp = &schema.SiteAIResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeAI, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// GetSiteMCP get site AI configuration\nfunc (s *siteInfoCommonService) GetSiteMCP(ctx context.Context) (resp *schema.SiteMCPResp, err error) {\n\tresp = &schema.SiteMCPResp{}\n\tif err = s.GetSiteInfoByType(ctx, constant.SiteTypeMCP, resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/siteinfo_common/siteinfo_service_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage siteinfo_common\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/service/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nvar (\n\tmockSiteInfoRepo *mock.MockSiteInfoRepo\n)\n\nfunc mockInit(ctl *gomock.Controller) {\n\tmockSiteInfoRepo = mock.NewMockSiteInfoRepo(ctl)\n\tmockSiteInfoRepo.EXPECT().GetByType(gomock.Any(), constant.SiteTypeGeneral).\n\t\tReturn(&entity.SiteInfo{Content: `{\"name\":\"name\"}`}, true, nil)\n}\n\nfunc TestSiteInfoCommonService_GetSiteGeneral(t *testing.T) {\n\tctl := gomock.NewController(t)\n\tdefer ctl.Finish()\n\tmockInit(ctl)\n\tsiteInfoCommonService := NewSiteInfoCommonService(mockSiteInfoRepo)\n\tresp, err := siteInfoCommonService.GetSiteGeneral(context.TODO())\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"name\", resp.Name)\n}\n"
  },
  {
    "path": "internal/service/tag/tag_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage tag\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\ttagcommonser \"github.com/apache/answer/internal/service/tag_common\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/jinzhu/copier\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity_common\"\n\t\"github.com/apache/answer/internal/service/permission\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// TagService user service\ntype TagService struct {\n\ttagRepo              tagcommonser.TagRepo\n\ttagCommonService     *tagcommonser.TagCommonService\n\trevisionService      *revision_common.RevisionService\n\tfollowCommon         activity_common.FollowRepo\n\tsiteInfoService      siteinfo_common.SiteInfoCommonService\n\tactivityQueueService activityqueue.Service\n}\n\n// NewTagService new tag service\nfunc NewTagService(\n\ttagRepo tagcommonser.TagRepo,\n\ttagCommonService *tagcommonser.TagCommonService,\n\trevisionService *revision_common.RevisionService,\n\tfollowCommon activity_common.FollowRepo,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tactivityQueueService activityqueue.Service,\n) *TagService {\n\treturn &TagService{\n\t\ttagRepo:              tagRepo,\n\t\ttagCommonService:     tagCommonService,\n\t\trevisionService:      revisionService,\n\t\tfollowCommon:         followCommon,\n\t\tsiteInfoService:      siteInfoService,\n\t\tactivityQueueService: activityQueueService,\n\t}\n}\n\n// RemoveTag delete tag\nfunc (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (err error) {\n\t// If the tag has associated problems, it cannot be deleted\n\ttagCount, err := ts.tagCommonService.CountTagRelByTagID(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif tagCount > 0 {\n\t\treturn errors.BadRequest(reason.TagIsUsedCannotDelete)\n\t}\n\n\t// If the tag has associated problems, it cannot be deleted\n\ttagSynonymCount, err := ts.tagRepo.GetTagSynonymCount(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif tagSynonymCount > 0 {\n\t\treturn errors.BadRequest(reason.TagIsUsedCannotDelete)\n\t}\n\n\t// tagRelRepo\n\terr = ts.tagRepo.RemoveTag(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tObjectID:         req.TagID,\n\t\tOriginalObjectID: req.TagID,\n\t\tActivityTypeKey:  constant.ActTagDeleted,\n\t})\n\treturn nil\n}\n\n// UpdateTag update tag\nfunc (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) {\n\treturn ts.tagCommonService.UpdateTag(ctx, req)\n}\n\n// RecoverTag recover tag\nfunc (ts *TagService) RecoverTag(ctx context.Context, req *schema.RecoverTagReq) (err error) {\n\ttagInfo, exist, err := ts.tagRepo.MustGetTagByNameOrID(ctx, req.TagID, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.TagNotFound)\n\t}\n\tif tagInfo.Status != entity.TagStatusDeleted {\n\t\treturn nil\n\t}\n\n\terr = ts.tagRepo.RecoverTag(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tTriggerUserID:    converter.StringToInt64(req.UserID),\n\t\tObjectID:         req.TagID,\n\t\tOriginalObjectID: req.TagID,\n\t\tActivityTypeKey:  constant.ActTagUndeleted,\n\t})\n\treturn nil\n}\n\n// GetTagInfo get tag one\nfunc (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) (resp *schema.GetTagResp, err error) {\n\tvar (\n\t\ttagInfo *entity.Tag\n\t\texist   bool\n\t)\n\tif len(req.ID) > 0 {\n\t\ttagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID)\n\t} else {\n\t\ttagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name)\n\t}\n\t// If user can recover deleted tag, try to search in all tags including deleted tags\n\tif !exist && req.CanRecover {\n\t\ttagInfo, exist, err = ts.tagRepo.MustGetTagByNameOrID(ctx, req.ID, req.Name)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.NotFound(reason.TagNotFound)\n\t}\n\n\tresp = &schema.GetTagResp{}\n\t// if tag is synonyms get original tag info\n\tif tagInfo.MainTagID > 0 {\n\t\ttagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !exist {\n\t\t\treturn nil, errors.NotFound(reason.TagNotFound)\n\t\t}\n\t\tresp.MainTagSlugName = tagInfo.SlugName\n\t}\n\tresp.TagID = tagInfo.ID\n\tresp.CreatedAt = tagInfo.CreatedAt.Unix()\n\tresp.UpdatedAt = tagInfo.UpdatedAt.Unix()\n\tresp.SlugName = tagInfo.SlugName\n\tresp.DisplayName = tagInfo.DisplayName\n\tresp.OriginalText = tagInfo.OriginalText\n\tresp.ParsedText = tagInfo.ParsedText\n\tresp.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, \"...\", 240)\n\tresp.FollowCount = tagInfo.FollowCount\n\tresp.QuestionCount = tagInfo.QuestionCount\n\tresp.Recommend = tagInfo.Recommend\n\tresp.Reserved = tagInfo.Reserved\n\tresp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID)\n\tresp.Status = entity.TagStatusDisplayMapping[tagInfo.Status]\n\tresp.MemberActions = permission.GetTagPermission(ctx, tagInfo.Status, req.CanEdit, req.CanDelete, req.CanMerge, req.CanRecover)\n\tresp.GetExcerpt()\n\treturn resp, nil\n}\n\n// GetTagsBySlugName get tags by slug name\nfunc (ts *TagService) GetTagsBySlugName(ctx context.Context, req *schema.SearchTagsBySlugName) (\n\tresp []*schema.GetTagBasicResp, err error) {\n\tresp = make([]*schema.GetTagBasicResp, 0)\n\ttagSlugNames := strings.Split(req.Tags, \",\")\n\tif len(tagSlugNames) == 0 {\n\t\treturn resp, nil\n\t}\n\ttagList, err := ts.tagCommonService.GetTagListByNames(ctx, tagSlugNames)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tfor _, tag := range tagList {\n\t\ttagItem := &schema.GetTagBasicResp{}\n\t\t_ = copier.Copy(tagItem, tag)\n\t\tresp = append(resp, tagItem)\n\t}\n\treturn resp, nil\n}\n\n// GetFollowingTags get following tags\nfunc (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (\n\tresp []*schema.GetFollowingTagsResp, err error) {\n\tresp = make([]*schema.GetFollowingTagsResp, 0)\n\tif len(userID) == 0 {\n\t\treturn resp, nil\n\t}\n\tobjIDs, err := ts.followCommon.GetFollowIDs(ctx, userID, entity.Tag{}.TableName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, t := range tagList {\n\t\ttagInfo := &schema.GetFollowingTagsResp{\n\t\t\tTagID:       t.ID,\n\t\t\tSlugName:    t.SlugName,\n\t\t\tDisplayName: t.DisplayName,\n\t\t\tRecommend:   t.Recommend,\n\t\t\tReserved:    t.Reserved,\n\t\t}\n\t\tif t.MainTagID > 0 {\n\t\t\tmainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif exist {\n\t\t\t\ttagInfo.MainTagSlugName = mainTag.SlugName\n\t\t\t}\n\t\t}\n\t\tresp = append(resp, tagInfo)\n\t}\n\treturn resp, nil\n}\n\n// GetTagSynonyms get tag synonyms\nfunc (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) (\n\tresp *schema.GetTagSynonymsResp, err error) {\n\tresp = &schema.GetTagSynonymsResp{Synonyms: make([]*schema.TagSynonym, 0)}\n\ttag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.TagNotFound)\n\t}\n\n\tvar tagList []*entity.Tag\n\tvar mainTagSlugName string\n\tif tag.MainTagID > 0 {\n\t\ttagList, err = ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: tag.MainTagID})\n\t} else {\n\t\ttagList, err = ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tag.ID)})\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// get main tag slug name\n\tif tag.MainTagID > 0 {\n\t\tfor _, tagInfo := range tagList {\n\t\t\tif tag.MainTagID == 0 {\n\t\t\t\tmainTagSlugName = tagInfo.SlugName\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tmainTagSlugName = tag.SlugName\n\t}\n\n\tfor _, t := range tagList {\n\t\tresp.Synonyms = append(resp.Synonyms, &schema.TagSynonym{\n\t\t\tTagID:           t.ID,\n\t\t\tSlugName:        t.SlugName,\n\t\t\tDisplayName:     t.DisplayName,\n\t\t\tMainTagSlugName: mainTagSlugName,\n\t\t})\n\t}\n\tresp.MemberActions = permission.GetTagSynonymPermission(ctx, req.CanEdit)\n\treturn\n}\n\n// UpdateTagSynonym add tag synonym\nfunc (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTagSynonymReq) (err error) {\n\t// format tag slug name\n\treq.Format()\n\taddSynonymTagList := make([]string, 0)\n\tremoveSynonymTagList := make([]string, 0)\n\tmainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.TagNotFound)\n\t}\n\n\t// find all exist tag\n\tfor _, item := range req.SynonymTagList {\n\t\tif item.SlugName == mainTagInfo.SlugName {\n\t\t\treturn errors.BadRequest(reason.TagCannotSetSynonymAsItself)\n\t\t}\n\t\taddSynonymTagList = append(addSynonymTagList, item.SlugName)\n\t}\n\ttagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList)\n\tif err != nil {\n\t\treturn err\n\t}\n\texistTagMapping := make(map[string]*entity.Tag, 0)\n\tfor _, tag := range tagListInDB {\n\t\texistTagMapping[tag.SlugName] = tag\n\t}\n\n\t// add tag list\n\tneedAddTagList := make([]*entity.Tag, 0)\n\tfor _, tag := range req.SynonymTagList {\n\t\tif existTagMapping[tag.SlugName] != nil {\n\t\t\tcontinue\n\t\t}\n\t\titem := &entity.Tag{}\n\t\titem.SlugName = tag.SlugName\n\t\titem.DisplayName = tag.DisplayName\n\t\titem.OriginalText = tag.OriginalText\n\t\titem.ParsedText = tag.ParsedText\n\t\titem.Status = entity.TagStatusAvailable\n\t\titem.UserID = req.UserID\n\t\tneedAddTagList = append(needAddTagList, item)\n\t}\n\n\tif len(needAddTagList) > 0 {\n\t\terr = ts.tagCommonService.AddTagList(ctx, needAddTagList)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// update tag revision\n\t\tfor _, tag := range needAddTagList {\n\t\t\texistTagMapping[tag.SlugName] = tag\n\t\t\trevisionDTO := &schema.AddRevisionDTO{\n\t\t\t\tUserID:   req.UserID,\n\t\t\t\tObjectID: tag.ID,\n\t\t\t\tTitle:    tag.SlugName,\n\t\t\t}\n\t\t\ttagInfoJson, _ := json.Marshal(tag)\n\t\t\trevisionDTO.Content = string(tagInfoJson)\n\t\t\trevisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\t\tUserID:           req.UserID,\n\t\t\t\tObjectID:         tag.ID,\n\t\t\t\tOriginalObjectID: tag.ID,\n\t\t\t\tActivityTypeKey:  constant.ActTagCreated,\n\t\t\t\tRevisionID:       revisionID,\n\t\t\t})\n\t\t}\n\t}\n\n\t// get all old synonyms list\n\toldSynonymList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(mainTagInfo.ID)})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, oldSynonym := range oldSynonymList {\n\t\tif existTagMapping[oldSynonym.SlugName] == nil {\n\t\t\tremoveSynonymTagList = append(removeSynonymTagList, oldSynonym.SlugName)\n\t\t}\n\t}\n\n\t// remove old synonyms\n\tif len(removeSynonymTagList) > 0 {\n\t\terr = ts.tagRepo.UpdateTagSynonym(ctx, removeSynonymTagList, 0, \"\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// update new synonyms\n\tif len(addSynonymTagList) > 0 {\n\t\terr = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, converter.StringToInt64(req.TagID), mainTagInfo.SlugName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetTagWithPage get tag list page\nfunc (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWithPageReq) (pageModel *pager.PageModel, err error) {\n\ttag := &entity.Tag{}\n\t_ = copier.Copy(tag, req)\n\ttag.UserID = \"\"\n\n\tpage := req.Page\n\tpageSize := req.PageSize\n\n\ttags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresp := make([]*schema.GetTagPageResp, 0)\n\tfor _, tag := range tags {\n\t\titem := &schema.GetTagPageResp{\n\t\t\tTagID:         tag.ID,\n\t\t\tSlugName:      tag.SlugName,\n\t\t\tDescription:   htmltext.FetchExcerpt(tag.ParsedText, \"...\", 240),\n\t\t\tDisplayName:   tag.DisplayName,\n\t\t\tOriginalText:  tag.OriginalText,\n\t\t\tParsedText:    tag.ParsedText,\n\t\t\tFollowCount:   tag.FollowCount,\n\t\t\tQuestionCount: tag.QuestionCount,\n\t\t\tIsFollower:    ts.checkTagIsFollow(ctx, req.UserID, tag.ID),\n\t\t\tCreatedAt:     tag.CreatedAt.Unix(),\n\t\t\tUpdatedAt:     tag.UpdatedAt.Unix(),\n\t\t\tRecommend:     tag.Recommend,\n\t\t\tReserved:      tag.Reserved,\n\t\t}\n\t\titem.GetExcerpt()\n\t\tresp = append(resp, item)\n\t}\n\treturn pager.NewPageModel(total, resp), nil\n}\n\n// MergeTag merge tag\nfunc (ts *TagService) MergeTag(ctx context.Context, req *schema.MergeTagReq) (err error) {\n\t// 1. get source tag and its synonyms\n\tsourceTag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.SourceTagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.TagNotFound)\n\t}\n\n\tsourceTagSynonyms, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(sourceTag.ID)})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\taddSynonymTagList := make([]string, 0)\n\taddSynonymTagList = append(addSynonymTagList, sourceTag.SlugName)\n\tfor _, tag := range sourceTagSynonyms {\n\t\taddSynonymTagList = append(addSynonymTagList, tag.SlugName)\n\t}\n\n\t// 2. get target tag\n\ttargetTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TargetTagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.TagNotFound)\n\t}\n\n\t// 3. update source tag and its synonyms as synonyms of target tag\n\tif len(addSynonymTagList) > 0 {\n\t\terr = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, converter.StringToInt64(targetTagInfo.ID), targetTagInfo.SlugName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// 4. update tag followers\n\terr = ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, targetTagInfo.ID, \"follow\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 5. update question tags\n\terr = ts.tagCommonService.MigrateTagQuestions(ctx, sourceTag.ID, targetTagInfo.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ts.tagCommonService.RefreshTagQuestionCount(ctx, []string{targetTagInfo.ID, sourceTag.ID})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// checkTagIsFollow get tag list page\nfunc (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string) bool {\n\tif len(userID) == 0 {\n\t\treturn false\n\t}\n\tfollowed, err := ts.followCommon.IsFollowed(ctx, userID, tagID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn followed\n}\n"
  },
  {
    "path": "internal/service/tag_common/tag_common.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage tag_common\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activityqueue\"\n\t\"github.com/apache/answer/internal/service/revision_common\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype TagCommonRepo interface {\n\tAddTagList(ctx context.Context, tagList []*entity.Tag) (err error)\n\tGetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)\n\tGetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)\n\tGetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error)\n\tGetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)\n\tGetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error)\n\tGetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)\n\tGetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error)\n\tGetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error)\n\tUpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error)\n\tUpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)\n}\n\ntype TagRepo interface {\n\tRemoveTag(ctx context.Context, tagID string) (err error)\n\tUpdateTag(ctx context.Context, tag *entity.Tag) (err error)\n\tRecoverTag(ctx context.Context, tagID string) (err error)\n\tMustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (tag *entity.Tag, exist bool, err error)\n\tUpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)\n\tGetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error)\n\tGetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error)\n\tGetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)\n}\n\ntype TagRelRepo interface {\n\tAddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error)\n\tRemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error)\n\tRecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error)\n\tShowTagRelListByObjectID(ctx context.Context, objectID string) (err error)\n\tHideTagRelListByObjectID(ctx context.Context, objectID string) (err error)\n\tRemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error)\n\tEnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error)\n\tGetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error)\n\tGetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error)\n\tBatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error)\n\tCountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error)\n\tGetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error)\n\tMigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error\n}\n\n// TagCommonService user service\ntype TagCommonService struct {\n\trevisionService      *revision_common.RevisionService\n\ttagCommonRepo        TagCommonRepo\n\ttagRelRepo           TagRelRepo\n\ttagRepo              TagRepo\n\tsiteInfoService      siteinfo_common.SiteInfoCommonService\n\tactivityQueueService activityqueue.Service\n}\n\n// NewTagCommonService new tag service\nfunc NewTagCommonService(\n\ttagCommonRepo TagCommonRepo,\n\ttagRelRepo TagRelRepo,\n\ttagRepo TagRepo,\n\trevisionService *revision_common.RevisionService,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tactivityQueueService activityqueue.Service,\n) *TagCommonService {\n\treturn &TagCommonService{\n\t\ttagCommonRepo:        tagCommonRepo,\n\t\ttagRelRepo:           tagRelRepo,\n\t\ttagRepo:              tagRepo,\n\t\trevisionService:      revisionService,\n\t\tsiteInfoService:      siteInfoService,\n\t\tactivityQueueService: activityQueueService,\n\t}\n}\n\n// SearchTagLike get tag list all\nfunc (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.GetTagBasicResp, err error) {\n\ttags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, len(req.Tag) == 0, false)\n\tif err != nil {\n\t\treturn\n\t}\n\tts.TagsFormatRecommendAndReserved(ctx, tags)\n\tmainTagId := make([]string, 0)\n\tfor _, tag := range tags {\n\t\tif tag.MainTagID != 0 {\n\t\t\tmainTagId = append(mainTagId, converter.IntToString(tag.MainTagID))\n\t\t}\n\t}\n\tmainTagMap := make(map[string]*entity.Tag)\n\tif len(mainTagId) > 0 {\n\t\tmainTagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, mainTagId)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, tag := range mainTagList {\n\t\t\tmainTagMap[tag.ID] = tag\n\t\t}\n\t}\n\tfor _, tag := range tags {\n\t\tif tag.MainTagID == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tmainTagID := converter.IntToString(tag.MainTagID)\n\t\tif _, ok := mainTagMap[mainTagID]; ok {\n\t\t\ttag.ID = mainTagMap[mainTagID].ID\n\t\t\ttag.SlugName = mainTagMap[mainTagID].SlugName\n\t\t\ttag.DisplayName = mainTagMap[mainTagID].DisplayName\n\t\t\ttag.Reserved = mainTagMap[mainTagID].Reserved\n\t\t\ttag.Recommend = mainTagMap[mainTagID].Recommend\n\t\t}\n\t}\n\tresp = make([]schema.GetTagBasicResp, 0)\n\trepetitiveTag := make(map[string]bool)\n\tfor _, tag := range tags {\n\t\tif _, ok := repetitiveTag[tag.SlugName]; !ok {\n\t\t\titem := schema.GetTagBasicResp{}\n\t\t\titem.TagID = tag.ID\n\t\t\titem.SlugName = tag.SlugName\n\t\t\titem.DisplayName = tag.DisplayName\n\t\t\titem.Recommend = tag.Recommend\n\t\t\titem.Reserved = tag.Reserved\n\t\t\tresp = append(resp, item)\n\t\t\trepetitiveTag[tag.SlugName] = true\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) {\n\ttags = make([]*schema.SiteWriteTag, 0)\n\tlist, err := ts.tagCommonRepo.GetRecommendTagList(ctx)\n\tif err != nil {\n\t\treturn tags, err\n\t}\n\tfor _, item := range list {\n\t\ttags = append(tags, &schema.SiteWriteTag{\n\t\t\tSlugName:    item.SlugName,\n\t\t\tDisplayName: item.DisplayName,\n\t\t})\n\t}\n\treturn tags, nil\n}\n\nfunc (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) {\n\ttags = make([]*schema.SiteWriteTag, 0)\n\tlist, err := ts.tagCommonRepo.GetReservedTagList(ctx)\n\tif err != nil {\n\t\treturn tags, err\n\t}\n\tfor _, item := range list {\n\t\ttags = append(tags, &schema.SiteWriteTag{\n\t\t\tSlugName:    item.SlugName,\n\t\t\tDisplayName: item.DisplayName,\n\t\t})\n\t}\n\treturn tags, nil\n}\n\nfunc (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, reservedTags []string, userID string) (\n\terrFields []*validator.FormErrorField, err error) {\n\trecommendErr := ts.CheckTag(ctx, recommendTags, userID)\n\treservedErr := ts.CheckTag(ctx, reservedTags, userID)\n\tif recommendErr != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"recommend_tags\",\n\t\t\tErrorMsg:   recommendErr.Error(),\n\t\t})\n\t\terr = recommendErr\n\t}\n\tif reservedErr != nil {\n\t\terrFields = append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"reserved_tags\",\n\t\t\tErrorMsg:   reservedErr.Error(),\n\t\t})\n\t\terr = reservedErr\n\t}\n\tif len(errFields) > 0 {\n\t\treturn errFields, err\n\t}\n\n\terr = ts.SetTagsAttribute(ctx, recommendTags, \"recommend\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = ts.SetTagsAttribute(ctx, reservedTags, \"reserved\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nil, nil\n}\n\n// SetTagsAttribute\nfunc (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) {\n\tvar oldTags []*entity.Tag\n\tswitch attribute {\n\tcase \"recommend\":\n\t\toldTags, err = ts.tagCommonRepo.GetRecommendTagList(ctx)\n\tcase \"reserved\":\n\t\toldTags, err = ts.tagCommonRepo.GetReservedTagList(ctx)\n\tdefault:\n\t\treturn\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\toldTagSlugNameList := make([]string, 0)\n\tfor _, tag := range oldTags {\n\t\toldTagSlugNameList = append(oldTagSlugNameList, tag.SlugName)\n\t}\n\n\terr = ts.tagCommonRepo.UpdateTagsAttribute(ctx, oldTagSlugNameList, attribute, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) {\n\tfor k, tagname := range tagNames {\n\t\ttagNames[k] = strings.ToLower(tagname)\n\t}\n\ttagList, err := ts.tagCommonRepo.GetTagListByNames(ctx, tagNames)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tts.TagsFormatRecommendAndReserved(ctx, tagList)\n\treturn tagList, nil\n}\n\nfunc (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) {\n\ttaginfo, err := ts.siteInfoService.GetSiteTag(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif !taginfo.RequiredTag || len(taginfo.RecommendTags) == 0 {\n\t\treturn true, nil\n\t}\n\ttagNames := make([]string, 0)\n\tfor _, item := range tags {\n\t\titem.SlugName = strings.ReplaceAll(item.SlugName, \" \", \"-\")\n\t\ttagNames = append(tagNames, item.SlugName)\n\t}\n\tlist, err := ts.GetTagListByNames(ctx, tagNames)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, item := range list {\n\t\tif item.Recommend {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc (ts *TagCommonService) GetMinimumTags(ctx context.Context) (int, error) {\n\tsiteInfo, err := ts.siteInfoService.GetSiteQuestion(ctx)\n\tif err != nil {\n\t\treturn 1, err\n\t}\n\tminimumTags := siteInfo.MinimumTags\n\treturn minimumTags, nil\n}\n\nfunc (ts *TagCommonService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) {\n\ttagNames := make([]string, 0)\n\ttagMap := make(map[string]bool)\n\tfor _, item := range tags {\n\t\titem.SlugName = strings.ReplaceAll(item.SlugName, \" \", \"-\")\n\t\ttagNames = append(tagNames, item.SlugName)\n\t\ttagMap[item.SlugName] = false\n\t}\n\tlist, err := ts.GetTagListByNames(ctx, tagNames)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\tfor _, item := range list {\n\t\t_, ok := tagMap[item.SlugName]\n\t\tif ok {\n\t\t\ttagMap[item.SlugName] = true\n\t\t}\n\t}\n\tfor _, has := range tagMap {\n\t\tif !has {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// GetObjectTag get object tag\nfunc (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) {\n\ttagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ts.TagFormat(ctx, tagsInfoList)\n}\n\n// AddTag get object tag\nfunc (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (resp *schema.AddTagResp, err error) {\n\t_, exist, err := ts.GetTagBySlugName(ctx, req.SlugName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\treturn nil, errors.BadRequest(reason.TagAlreadyExist)\n\t}\n\tslugName := strings.ReplaceAll(req.SlugName, \" \", \"-\")\n\tslugName = strings.ToLower(slugName)\n\ttagInfo := &entity.Tag{\n\t\tSlugName:     slugName,\n\t\tDisplayName:  req.DisplayName,\n\t\tOriginalText: req.OriginalText,\n\t\tParsedText:   req.ParsedText,\n\t\tStatus:       entity.TagStatusAvailable,\n\t\tUserID:       req.UserID,\n\t}\n\ttagList := []*entity.Tag{tagInfo}\n\terr = ts.tagCommonRepo.AddTagList(ctx, tagList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   req.UserID,\n\t\tObjectID: tagInfo.ID,\n\t\tTitle:    tagInfo.SlugName,\n\t}\n\ttagInfoJson, _ := json.Marshal(tagInfo)\n\trevisionDTO.Content = string(tagInfoJson)\n\trevisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\tUserID:           req.UserID,\n\t\tObjectID:         tagInfo.ID,\n\t\tOriginalObjectID: tagInfo.ID,\n\t\tActivityTypeKey:  constant.ActTagCreated,\n\t\tRevisionID:       revisionID,\n\t})\n\treturn &schema.AddTagResp{SlugName: tagInfo.SlugName}, nil\n}\n\n// AddTagList get object tag\nfunc (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {\n\treturn ts.tagCommonRepo.AddTagList(ctx, tagList)\n}\n\n// GetTagByID get object tag\nfunc (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) {\n\ttag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID, false)\n\tif !exist {\n\t\treturn\n\t}\n\tts.tagFormatRecommendAndReserved(ctx, tag)\n\treturn\n}\n\n// GetTagIDsByMainTagID get object tag\nfunc (ts *TagCommonService) GetTagIDsByMainTagID(ctx context.Context, tagID string) (tagIDs []string, err error) {\n\ttagIDs, err = ts.tagRepo.GetIDsByMainTagId(ctx, tagID)\n\treturn\n}\n\n// GetTagBySlugName get object tag\nfunc (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) {\n\ttag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName)\n\tif !exist {\n\t\treturn\n\t}\n\tts.tagFormatRecommendAndReserved(ctx, tag)\n\treturn\n}\n\n// GetTagListByIDs get object tag\nfunc (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {\n\ttagList, err = ts.tagCommonRepo.GetTagListByIDs(ctx, ids)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tts.TagsFormatRecommendAndReserved(ctx, tagList)\n\treturn\n}\n\n// GetTagPage get object tag\nfunc (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (\n\ttagList []*entity.Tag, total int64, err error) {\n\ttagList, total, err = ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, queryCond)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tts.TagsFormatRecommendAndReserved(ctx, tagList)\n\treturn\n}\n\nfunc (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) {\n\ttagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttagIDList := make([]string, 0)\n\tfor _, tag := range tagList {\n\t\ttagIDList = append(tagIDList, tag.TagID)\n\t}\n\tobjTags, err = ts.GetTagListByIDs(ctx, tagIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn objTags, nil\n}\n\nfunc (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (objTags []*schema.TagResp, err error) {\n\tobjTags = make([]*schema.TagResp, 0)\n\tfor _, tagInfo := range tags {\n\t\tobjTags = append(objTags, &schema.TagResp{\n\t\t\tSlugName:        tagInfo.SlugName,\n\t\t\tDisplayName:     tagInfo.DisplayName,\n\t\t\tMainTagSlugName: tagInfo.MainTagSlugName,\n\t\t\tRecommend:       tagInfo.Recommend,\n\t\t\tReserved:        tagInfo.Reserved,\n\t\t})\n\t}\n\treturn objTags, nil\n}\n\nfunc (ts *TagCommonService) TagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) {\n\tif len(tagList) == 0 {\n\t\treturn\n\t}\n\ttagConfig, err := ts.siteInfoService.GetSiteTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !tagConfig.RequiredTag {\n\t\tfor _, tag := range tagList {\n\t\t\ttag.Recommend = false\n\t\t}\n\t}\n}\n\nfunc (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, tag *entity.Tag) {\n\tif tag == nil {\n\t\treturn\n\t}\n\ttagConfig, err := ts.siteInfoService.GetSiteTag(ctx)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\tif !tagConfig.RequiredTag {\n\t\ttag.Recommend = false\n\t}\n}\n\n// BatchGetObjectTag batch get object tag\nfunc (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) {\n\tobjectIDTagMap := make(map[string][]*schema.TagResp)\n\tif len(objectIds) == 0 {\n\t\treturn objectIDTagMap, nil\n\t}\n\tobjectTagRelList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds)\n\tif err != nil {\n\t\treturn objectIDTagMap, err\n\t}\n\ttagIDList := make([]string, 0)\n\tfor _, tag := range objectTagRelList {\n\t\ttagIDList = append(tagIDList, tag.TagID)\n\t}\n\ttagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList)\n\tif err != nil {\n\t\treturn objectIDTagMap, err\n\t}\n\ttagsInfoMapping := make(map[string]*entity.Tag)\n\ttagsRank := make(map[string]int) // Used for sorting\n\tfor idx, item := range tagsInfoList {\n\t\ttagsInfoMapping[item.ID] = item\n\t\ttagsRank[item.ID] = idx\n\t}\n\n\tfor _, item := range objectTagRelList {\n\t\t_, ok := tagsInfoMapping[item.TagID]\n\t\tif ok {\n\t\t\ttagInfo := tagsInfoMapping[item.TagID]\n\t\t\tt := &schema.TagResp{\n\t\t\t\tID:              tagInfo.ID,\n\t\t\t\tSlugName:        tagInfo.SlugName,\n\t\t\t\tDisplayName:     tagInfo.DisplayName,\n\t\t\t\tMainTagSlugName: tagInfo.MainTagSlugName,\n\t\t\t\tRecommend:       tagInfo.Recommend,\n\t\t\t\tReserved:        tagInfo.Reserved,\n\t\t\t}\n\t\t\tobjectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t)\n\t\t}\n\t}\n\t// The sorting in tagsRank is correct, object tags should be sorted by tagsRank\n\tfor _, objectTags := range objectIDTagMap {\n\t\tsort.SliceStable(objectTags, func(i, j int) bool {\n\t\t\treturn tagsRank[objectTags[i].ID] < tagsRank[objectTags[j].ID]\n\t\t})\n\t}\n\treturn objectIDTagMap, nil\n}\n\nfunc (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) {\n\tif len(tags) == 0 {\n\t\treturn nil\n\t}\n\n\t// find tags name\n\ttagListInDb, err := ts.GetTagListByNames(ctx, tags)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttagInDbMapping := make(map[string]*entity.Tag)\n\tchecktags := make([]string, 0)\n\n\tfor _, tag := range tagListInDb {\n\t\tif tag.MainTagID != 0 {\n\t\t\tchecktags = append(checktags, fmt.Sprintf(\"\\\"%s\\\"\", tag.SlugName))\n\t\t}\n\t\ttagInDbMapping[tag.SlugName] = tag\n\t}\n\tif len(checktags) > 0 {\n\t\terr = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf(\"Should not contain synonym tags %s\", strings.Join(checktags, \",\")))\n\t\treturn err\n\t}\n\n\taddTagList := make([]*entity.Tag, 0)\n\taddTagMsgList := make([]string, 0)\n\tfor _, tag := range tags {\n\t\t_, ok := tagInDbMapping[tag]\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\t\titem := &entity.Tag{}\n\t\titem.SlugName = tag\n\t\titem.DisplayName = tag\n\t\titem.OriginalText = \"\"\n\t\titem.ParsedText = \"\"\n\t\titem.Status = entity.TagStatusAvailable\n\t\titem.UserID = userID\n\t\taddTagList = append(addTagList, item)\n\t\taddTagMsgList = append(addTagMsgList, tag)\n\t}\n\n\tif len(addTagList) > 0 {\n\t\terr = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf(\"tag [%s] does not exist\",\n\t\t\tstrings.Join(addTagMsgList, \",\")))\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// CheckTagsIsChange\nfunc (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList, oldtagNameList []string) bool {\n\tcheck := make(map[string]bool)\n\tif len(tagNameList) != len(oldtagNameList) {\n\t\treturn true\n\t}\n\tfor _, item := range tagNameList {\n\t\tcheck[item] = false\n\t}\n\tfor _, item := range oldtagNameList {\n\t\t_, ok := check[item]\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tcheck[item] = true\n\t}\n\tfor _, value := range check {\n\t\tif !value {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {\n\treservedTagsMap := make(map[string]bool)\n\tneedTagsMap := make([]string, 0)\n\tnotNeedTagsMap := make([]string, 0)\n\tfor _, tag := range objectTagData {\n\t\tif tag.Reserved {\n\t\t\treservedTagsMap[tag.SlugName] = true\n\t\t}\n\t}\n\tfor _, tag := range oldobjectTagData {\n\t\tif tag.Reserved {\n\t\t\t_, ok := reservedTagsMap[tag.SlugName]\n\t\t\tif !ok {\n\t\t\t\tneedTagsMap = append(needTagsMap, tag.SlugName)\n\t\t\t} else {\n\t\t\t\treservedTagsMap[tag.SlugName] = false\n\t\t\t}\n\t\t}\n\t}\n\n\tfor k, v := range reservedTagsMap {\n\t\tif v {\n\t\t\tnotNeedTagsMap = append(notNeedTagsMap, k)\n\t\t}\n\t}\n\n\tif len(needTagsMap) > 0 {\n\t\treturn false, true, needTagsMap, []string{}\n\t}\n\n\tif len(notNeedTagsMap) > 0 {\n\t\treturn true, false, []string{}, notNeedTagsMap\n\t}\n\n\treturn true, true, []string{}, []string{}\n}\n\n// ObjectChangeTag change object tag list\nfunc (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange, minimumTags int) (errorlist []*validator.FormErrorField, err error) {\n\t// checks if the tags sent in the put req are less than the minimum, if so, tag changes are not applied\n\tif len(objectTagData.Tags) < minimumTags {\n\t\terrorlist := make([]*validator.FormErrorField, 0)\n\t\terrorlist = append(errorlist, &validator.FormErrorField{\n\t\t\tErrorField: \"tags\",\n\t\t\tErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),\n\t\t})\n\n\t\terr = errors.BadRequest(reason.TagMinCount)\n\t\treturn errorlist, err\n\t}\n\n\tthisObjTagNameList := make([]string, 0)\n\tthisObjTagIDList := make([]string, 0)\n\tfor _, t := range objectTagData.Tags {\n\t\tt.SlugName = strings.ToLower(t.SlugName)\n\t\tthisObjTagNameList = append(thisObjTagNameList, t.SlugName)\n\t}\n\n\t// find tags name\n\ttagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttagInDbMapping := make(map[string]*entity.Tag)\n\tfor _, tag := range tagListInDb {\n\t\ttagInDbMapping[strings.ToLower(tag.SlugName)] = tag\n\t\tthisObjTagIDList = append(thisObjTagIDList, tag.ID)\n\t}\n\n\taddTagList := make([]*entity.Tag, 0)\n\tfor _, tag := range objectTagData.Tags {\n\t\t_, ok := tagInDbMapping[strings.ToLower(tag.SlugName)]\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\t\titem := &entity.Tag{}\n\t\titem.SlugName = strings.ReplaceAll(tag.SlugName, \" \", \"-\")\n\t\titem.DisplayName = tag.DisplayName\n\t\titem.OriginalText = tag.OriginalText\n\t\titem.ParsedText = tag.ParsedText\n\t\titem.Status = entity.TagStatusAvailable\n\t\titem.UserID = objectTagData.UserID\n\t\taddTagList = append(addTagList, item)\n\t}\n\n\tif len(addTagList) > 0 {\n\t\terr = ts.tagCommonRepo.AddTagList(ctx, addTagList)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, tag := range addTagList {\n\t\t\tthisObjTagIDList = append(thisObjTagIDList, tag.ID)\n\t\t\trevisionDTO := &schema.AddRevisionDTO{\n\t\t\t\tUserID:   objectTagData.UserID,\n\t\t\t\tObjectID: tag.ID,\n\t\t\t\tTitle:    tag.SlugName,\n\t\t\t}\n\t\t\ttagInfoJson, _ := json.Marshal(tag)\n\t\t\trevisionDTO.Content = string(tagInfoJson)\n\t\t\trevisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\t\tUserID:           objectTagData.UserID,\n\t\t\t\tObjectID:         tag.ID,\n\t\t\t\tOriginalObjectID: tag.ID,\n\t\t\t\tActivityTypeKey:  constant.ActTagCreated,\n\t\t\t\tRevisionID:       revisionID,\n\t\t\t})\n\t\t}\n\t}\n\n\terr = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectID, thisObjTagIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nil, nil\n}\n\nfunc (ts *TagCommonService) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) {\n\treturn ts.tagRelRepo.CountTagRelByTagID(ctx, tagID)\n}\n\n// RefreshTagQuestionCount refresh tag question count\nfunc (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs []string) (err error) {\n\tfor _, tagID := range tagIDs {\n\t\tcount, err := ts.tagRelRepo.CountTagRelByTagID(ctx, tagID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = ts.tagCommonRepo.UpdateTagQuestionCount(ctx, tagID, int(count))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Debugf(\"tag count updated %s %d\", tagID, count)\n\t}\n\treturn nil\n}\n\nfunc (ts *TagCommonService) RefreshTagCountByQuestionID(ctx context.Context, questionID string) (err error) {\n\ttagListList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, questionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttagIDs := make([]string, 0)\n\tfor _, item := range tagListList {\n\t\ttagIDs = append(tagIDs, item.TagID)\n\t}\n\terr = ts.RefreshTagQuestionCount(ctx, tagIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// RemoveTagRelListByObjectID remove tag relation by object id\nfunc (ts *TagCommonService) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\treturn ts.tagRelRepo.RemoveTagRelListByObjectID(ctx, objectID)\n}\n\n// RecoverTagRelListByObjectID recover tag relation by object id\nfunc (ts *TagCommonService) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\treturn ts.tagRelRepo.RecoverTagRelListByObjectID(ctx, objectID)\n}\n\nfunc (ts *TagCommonService) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\treturn ts.tagRelRepo.HideTagRelListByObjectID(ctx, objectID)\n}\n\nfunc (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {\n\treturn ts.tagRelRepo.ShowTagRelListByObjectID(ctx, objectID)\n}\n\n// CreateOrUpdateTagRelList if tag relation is exists update status, if not create it\nfunc (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) {\n\taddTagIDMapping := make(map[string]struct{})\n\tfor _, t := range tagIDs {\n\t\taddTagIDMapping[t] = struct{}{}\n\t}\n\n\t// get all old relation\n\toldTagRelList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar deleteTagRel []int64\n\tneedRefreshTagIDs := make([]string, 0, len(oldTagRelList)+len(tagIDs))\n\tneedRefreshTagIDs = append(needRefreshTagIDs, tagIDs...)\n\tfor _, rel := range oldTagRelList {\n\t\tif _, ok := addTagIDMapping[rel.TagID]; !ok {\n\t\t\tdeleteTagRel = append(deleteTagRel, rel.ID)\n\t\t\tneedRefreshTagIDs = append(needRefreshTagIDs, rel.TagID)\n\t\t}\n\t}\n\n\taddTagRelList := make([]*entity.TagRel, 0)\n\tenableTagRelList := make([]int64, 0)\n\tdefaultTagRelStatus, err := ts.tagRelRepo.GetTagRelDefaultStatusByObjectID(ctx, objectId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, tagID := range tagIDs {\n\t\trel, exist, err := ts.tagRelRepo.GetObjectTagRelWithoutStatus(ctx, objectId, tagID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// if not exist add tag relation\n\t\tif !exist {\n\t\t\taddTagRelList = append(addTagRelList, &entity.TagRel{\n\t\t\t\tTagID: tagID, ObjectID: objectId, Status: defaultTagRelStatus,\n\t\t\t})\n\t\t}\n\t\t// if exist and has been removed, that should be enabled\n\t\tif exist && rel.Status != entity.TagRelStatusAvailable && rel.Status != entity.TagRelStatusHide {\n\t\t\tenableTagRelList = append(enableTagRelList, rel.ID)\n\t\t}\n\t}\n\n\tif len(deleteTagRel) > 0 {\n\t\tif err = ts.tagRelRepo.RemoveTagRelListByIDs(ctx, deleteTagRel); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(addTagRelList) > 0 {\n\t\tif err = ts.tagRelRepo.AddTagRelList(ctx, addTagRelList); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(enableTagRelList) > 0 {\n\t\tif err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList, defaultTagRelStatus == entity.TagRelStatusHide); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = ts.RefreshTagQuestionCount(ctx, needRefreshTagIDs)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn nil\n}\n\nfunc (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) {\n\tvar canUpdate bool\n\t_, existUnreviewed, err := ts.revisionService.ExistUnreviewedByObjectID(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif existUnreviewed {\n\t\terr = errors.BadRequest(reason.AnswerCannotUpdate)\n\t\treturn err\n\t}\n\n\ttagInfo, exist, err := ts.GetTagByID(ctx, req.TagID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.TagNotFound)\n\t}\n\n\t// Adding equivalent slug formatting for tag update\n\tslugName := strings.ReplaceAll(req.SlugName, \" \", \"-\")\n\tslugName = strings.ToLower(slugName)\n\n\t// If the content is the same, ignore it\n\tif tagInfo.OriginalText == req.OriginalText &&\n\t\ttagInfo.DisplayName == req.DisplayName &&\n\t\ttagInfo.SlugName == slugName {\n\t\treturn nil\n\t}\n\n\ttagInfo.SlugName = slugName\n\ttagInfo.DisplayName = req.DisplayName\n\ttagInfo.OriginalText = req.OriginalText\n\ttagInfo.ParsedText = req.ParsedText\n\n\trevisionDTO := &schema.AddRevisionDTO{\n\t\tUserID:   req.UserID,\n\t\tObjectID: tagInfo.ID,\n\t\tTitle:    tagInfo.SlugName,\n\t\tLog:      req.EditSummary,\n\t}\n\n\tif req.NoNeedReview {\n\t\tcanUpdate = true\n\t\terr = ts.tagRepo.UpdateTag(ctx, tagInfo)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif tagInfo.MainTagID == 0 && len(req.SlugName) > 0 {\n\t\t\tlog.Debugf(\"tag %s update slug_name\", tagInfo.SlugName)\n\t\t\ttagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tupdateTagSlugNames := make([]string, 0)\n\t\t\tfor _, tag := range tagList {\n\t\t\t\tupdateTagSlugNames = append(updateTagSlugNames, tag.SlugName)\n\t\t\t}\n\t\t\terr = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\trevisionDTO.Status = entity.RevisionReviewPassStatus\n\t} else {\n\t\trevisionDTO.Status = entity.RevisionUnreviewedStatus\n\t}\n\n\ttagInfoJson, _ := json.Marshal(tagInfo)\n\trevisionDTO.Content = string(tagInfoJson)\n\trevisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif canUpdate {\n\t\tts.activityQueueService.Send(ctx, &schema.ActivityMsg{\n\t\t\tUserID:           req.UserID,\n\t\t\tObjectID:         tagInfo.ID,\n\t\t\tOriginalObjectID: tagInfo.ID,\n\t\t\tActivityTypeKey:  constant.ActTagEdited,\n\t\t\tRevisionID:       revisionID,\n\t\t})\n\t}\n\n\treturn\n}\n\n// MigrateTagQuestions migrate tag question\nfunc (ts *TagCommonService) MigrateTagQuestions(ctx context.Context, sourceTagID, targetTagID string) (err error) {\n\treturn ts.tagRelRepo.MigrateTagObjects(ctx, sourceTagID, targetTagID)\n}\n"
  },
  {
    "path": "internal/service/unique/uniqid_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage unique\n\nimport (\n\t\"context\"\n)\n\n// UniqueIDRepo unique id repository\ntype UniqueIDRepo interface {\n\tGenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error)\n}\n"
  },
  {
    "path": "internal/service/uploader/upload.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage uploader\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/service/file_record\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/service/service_config\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/dir\"\n\t\"github.com/apache/answer/pkg/uid\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/gin-gonic/gin\"\n\texifremove \"github.com/scottleedavis/go-exif-remove\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nvar (\n\tsubPathList = []string{\n\t\tconstant.AvatarSubPath,\n\t\tconstant.AvatarThumbSubPath,\n\t\tconstant.PostSubPath,\n\t\tconstant.BrandingSubPath,\n\t\tconstant.FilesPostSubPath,\n\t\tconstant.DeletedSubPath,\n\t}\n\tsupportedThumbFileExtMapping = map[string]imaging.Format{\n\t\t\".jpg\":  imaging.JPEG,\n\t\t\".jpeg\": imaging.JPEG,\n\t\t\".png\":  imaging.PNG,\n\t\t\".gif\":  imaging.GIF,\n\t}\n)\n\ntype UploaderService interface {\n\tUploadAvatarFile(ctx *gin.Context, userID string) (url string, err error)\n\tUploadPostFile(ctx *gin.Context, userID string) (url string, err error)\n\tUploadPostAttachment(ctx *gin.Context, userID string) (url string, err error)\n\tUploadBrandingFile(ctx *gin.Context, userID string) (url string, err error)\n\tAvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error)\n}\n\n// uploaderService uploader service\ntype uploaderService struct {\n\tserviceConfig     *service_config.ServiceConfig\n\tsiteInfoService   siteinfo_common.SiteInfoCommonService\n\tfileRecordService *file_record.FileRecordService\n}\n\n// NewUploaderService new upload service\nfunc NewUploaderService(\n\tserviceConfig *service_config.ServiceConfig,\n\tsiteInfoService siteinfo_common.SiteInfoCommonService,\n\tfileRecordService *file_record.FileRecordService,\n) UploaderService {\n\tfor _, subPath := range subPathList {\n\t\terr := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn &uploaderService{\n\t\tserviceConfig:     serviceConfig,\n\t\tsiteInfoService:   siteInfoService,\n\t\tfileRecordService: fileRecordService,\n\t}\n}\n\n// UploadAvatarFile upload avatar file\nfunc (us *uploaderService) UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) {\n\turl, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(url) > 0 {\n\t\treturn url, nil\n\t}\n\n\tsiteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize())\n\tfile, fileHeader, err := ctx.Request.FormFile(\"file\")\n\tif err != nil {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\tfileExt := strings.ToLower(path.Ext(fileHeader.Filename))\n\tif _, ok := plugin.DefaultFileTypeCheckMapping[plugin.UserAvatar][fileExt]; !ok {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\n\tnewFilename := fmt.Sprintf(\"%s%s\", uid.IDStr12(), fileExt)\n\tavatarFilePath := path.Join(constant.AvatarSubPath, newFilename)\n\turl, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tus.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserAvatar))\n\treturn url, nil\n}\n\nfunc (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) {\n\tfileSuffix := path.Ext(fileName)\n\tif _, ok := supportedThumbFileExtMapping[fileSuffix]; !ok {\n\t\t// if file type is not supported, return original file\n\t\treturn path.Join(us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName), nil\n\t}\n\tif size > 1024 {\n\t\tsize = 1024\n\t}\n\n\tthumbFileName := fmt.Sprintf(\"%d_%d@%s\", size, size, fileName)\n\tthumbFilePath := fmt.Sprintf(\"%s/%s/%s\", us.serviceConfig.UploadPath, constant.AvatarThumbSubPath, thumbFileName)\n\t_, err = os.ReadFile(thumbFilePath)\n\tif err == nil {\n\t\treturn thumbFilePath, nil\n\t}\n\tfilePath := fmt.Sprintf(\"%s/%s/%s\", us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName)\n\tavatarFile, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn \"\", errors.NotFound(reason.UnknownError).WithError(err)\n\t}\n\treader := bytes.NewReader(avatarFile)\n\timg, err := imaging.Decode(reader)\n\tif err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\tvar buf bytes.Buffer\n\tnewImage := imaging.Fill(img, size, size, imaging.Center, imaging.Linear)\n\tif err = imaging.Encode(&buf, newImage, supportedThumbFileExtMapping[fileSuffix]); err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\tif err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, constant.AvatarThumbSubPath)); err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\tavatarFilePath := path.Join(constant.AvatarThumbSubPath, thumbFileName)\n\tsaveFilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath)\n\tout, err := os.Create(saveFilePath)\n\tif err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tdefer func() {\n\t\t_ = out.Close()\n\t}()\n\n\tthumbReader := bytes.NewReader(buf.Bytes())\n\tif _, err = io.Copy(out, thumbReader); err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\treturn saveFilePath, nil\n}\n\nfunc (us *uploaderService) UploadPostFile(ctx *gin.Context, userID string) (\n\turl string, err error) {\n\turl, err = us.tryToUploadByPlugin(ctx, plugin.UserPost)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(url) > 0 {\n\t\treturn url, nil\n\t}\n\n\tsiteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize())\n\tfile, fileHeader, err := ctx.Request.FormFile(\"file\")\n\tif err != nil {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\tif checker.IsUnAuthorizedExtension(fileHeader.Filename, siteAdvanced.AuthorizedImageExtensions) {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\n\tfileExt := strings.ToLower(path.Ext(fileHeader.Filename))\n\tnewFilename := fmt.Sprintf(\"%s%s\", uid.IDStr12(), fileExt)\n\tavatarFilePath := path.Join(constant.PostSubPath, newFilename)\n\turl, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tus.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserPost))\n\treturn url, nil\n}\n\nfunc (us *uploaderService) UploadPostAttachment(ctx *gin.Context, userID string) (\n\turl string, err error) {\n\turl, err = us.tryToUploadByPlugin(ctx, plugin.UserPostAttachment)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(url) > 0 {\n\t\treturn url, nil\n\t}\n\n\tresp, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, resp.GetMaxAttachmentSize())\n\tfile, fileHeader, err := ctx.Request.FormFile(\"file\")\n\tif err != nil {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\tif checker.IsUnAuthorizedExtension(fileHeader.Filename, resp.AuthorizedAttachmentExtensions) {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\n\tfileExt := strings.ToLower(path.Ext(fileHeader.Filename))\n\tnewFilename := fmt.Sprintf(\"%s%s\", uid.IDStr12(), fileExt)\n\tattachmentFilePath := path.Join(constant.FilesPostSubPath, newFilename)\n\turl, err = us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, attachmentFilePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tus.fileRecordService.AddFileRecord(ctx, userID, attachmentFilePath, url, string(plugin.UserPostAttachment))\n\treturn url, nil\n}\n\nfunc (us *uploaderService) UploadBrandingFile(ctx *gin.Context, userID string) (\n\turl string, err error) {\n\turl, err = us.tryToUploadByPlugin(ctx, plugin.AdminBranding)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(url) > 0 {\n\t\treturn url, nil\n\t}\n\n\tsiteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteAdvanced.GetMaxImageSize())\n\tfile, fileHeader, err := ctx.Request.FormFile(\"file\")\n\tif err != nil {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\tfileExt := strings.ToLower(path.Ext(fileHeader.Filename))\n\tif _, ok := plugin.DefaultFileTypeCheckMapping[plugin.AdminBranding][fileExt]; !ok {\n\t\treturn \"\", errors.BadRequest(reason.RequestFormatError).WithError(err)\n\t}\n\n\tnewFilename := fmt.Sprintf(\"%s%s\", uid.IDStr12(), fileExt)\n\tavatarFilePath := path.Join(constant.BrandingSubPath, newFilename)\n\turl, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tus.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.AdminBranding))\n\treturn url, nil\n}\n\nfunc (us *uploaderService) uploadImageFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (\n\turl string, err error) {\n\tsiteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tsiteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfilePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)\n\tif err := ctx.SaveUploadedFile(file, filePath); err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\tsrc, err := file.Open()\n\tif err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\tdefer func() {\n\t\t_ = src.Close()\n\t}()\n\n\tif !checker.DecodeAndCheckImageFile(filePath, siteAdvanced.GetMaxImageMegapixel()) {\n\t\treturn \"\", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)\n\t}\n\n\tif err := removeExif(filePath); err != nil {\n\t\tlog.Error(err)\n\t}\n\n\turl = fmt.Sprintf(\"%s/uploads/%s\", siteGeneral.SiteUrl, fileSubPath)\n\treturn url, nil\n}\n\nfunc (us *uploaderService) uploadAttachmentFile(ctx *gin.Context, file *multipart.FileHeader, originalFilename, fileSubPath string) (\n\tdownloadUrl string, err error) {\n\tsiteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfilePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)\n\tif err := ctx.SaveUploadedFile(file, filePath); err != nil {\n\t\treturn \"\", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()\n\t}\n\n\t// Need url encode the original filename. Because the filename may contain special characters that conflict with the markdown syntax.\n\toriginalFilename = url.QueryEscape(originalFilename)\n\n\t// The original filename is 123.pdf\n\t// The local saved path is /UploadPath/hash.pdf\n\t// When downloading, the download link will be redirect to the local saved path. And the download filename will be 123.png.\n\tdownloadPath := strings.TrimSuffix(fileSubPath, filepath.Ext(fileSubPath)) + \"/\" + originalFilename\n\tdownloadUrl = fmt.Sprintf(\"%s/uploads/%s\", siteGeneral.SiteUrl, downloadPath)\n\treturn downloadUrl, nil\n}\n\nfunc (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (\n\turl string, err error) {\n\tsiteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcond := plugin.UploadFileCondition{\n\t\tSource:                         source,\n\t\tMaxImageSize:                   siteAdvanced.MaxImageSize,\n\t\tMaxAttachmentSize:              siteAdvanced.MaxAttachmentSize,\n\t\tMaxImageMegapixel:              siteAdvanced.MaxImageMegapixel,\n\t\tAuthorizedImageExtensions:      siteAdvanced.AuthorizedImageExtensions,\n\t\tAuthorizedAttachmentExtensions: siteAdvanced.AuthorizedAttachmentExtensions,\n\t}\n\t_ = plugin.CallStorage(func(fn plugin.Storage) error {\n\t\tresp := fn.UploadFile(ctx, cond)\n\t\tif resp.OriginalError != nil {\n\t\t\tlog.Errorf(\"upload file by plugin failed, err: %v\", resp.OriginalError)\n\t\t\terr = errors.BadRequest(\"\").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err)\n\t\t} else {\n\t\t\turl = resp.FullURL\n\t\t}\n\t\treturn nil\n\t})\n\treturn url, err\n}\n\n// removeExif remove exif\n// only support jpg/jpeg/png\nfunc removeExif(path string) error {\n\text := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), \".\"))\n\tif ext != \"jpeg\" && ext != \"jpg\" && ext != \"png\" {\n\t\treturn nil\n\t}\n\timg, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnoExifBytes, err := exifremove.Remove(img)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, noExifBytes, 0644)\n}\n"
  },
  {
    "path": "internal/service/user_admin/user_backyard.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_admin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/mail\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/base/validator\"\n\tanswercommon \"github.com/apache/answer/internal/service/answer_common\"\n\t\"github.com/apache/answer/internal/service/badge\"\n\t\"github.com/apache/answer/internal/service/comment_common\"\n\t\"github.com/apache/answer/internal/service/export\"\n\tnotificationcommon \"github.com/apache/answer/internal/service/notification_common\"\n\t\"github.com/apache/answer/internal/service/plugin_common\"\n\tquestioncommon \"github.com/apache/answer/internal/service/question_common\"\n\t\"github.com/apache/answer/pkg/token\"\n\n\t\"github.com/apache/answer/internal/base/pager\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/internal/service/user_external_login\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// UserAdminRepo user repository\ntype UserAdminRepo interface {\n\tUpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string, suspendedUntil time.Time) (err error)\n\tGetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error)\n\tGetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error)\n\tGetUserPage(ctx context.Context, page, pageSize int, user *entity.User,\n\t\tusernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error)\n\tAddUser(ctx context.Context, user *entity.User) (err error)\n\tAddUsers(ctx context.Context, users []*entity.User) (err error)\n\tUpdateUserPassword(ctx context.Context, userID string, password string) (err error)\n\tDeletePermanentlyUsers(ctx context.Context) (err error)\n\tGetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error)\n}\n\n// UserAdminService user service\ntype UserAdminService struct {\n\tuserRepo              UserAdminRepo\n\tuserRoleRelService    *role.UserRoleRelService\n\tauthService           *auth.AuthService\n\tuserCommonService     *usercommon.UserCommon\n\tuserActivity          activity.UserActiveActivityRepo\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n\temailService          *export.EmailService\n\tquestionCommonRepo    questioncommon.QuestionRepo\n\tanswerCommonRepo      answercommon.AnswerRepo\n\tcommentCommonRepo     comment_common.CommentCommonRepo\n\tuserExternalLoginRepo user_external_login.UserExternalLoginRepo\n\tnotificationRepo      notificationcommon.NotificationRepo\n\tpluginUserConfigRepo  plugin_common.PluginUserConfigRepo\n\tbadgeAwardRepo        badge.BadgeAwardRepo\n}\n\n// NewUserAdminService new user admin service\nfunc NewUserAdminService(\n\tuserRepo UserAdminRepo,\n\tuserRoleRelService *role.UserRoleRelService,\n\tauthService *auth.AuthService,\n\tuserCommonService *usercommon.UserCommon,\n\tuserActivity activity.UserActiveActivityRepo,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n\temailService *export.EmailService,\n\tquestionCommonRepo questioncommon.QuestionRepo,\n\tanswerCommonRepo answercommon.AnswerRepo,\n\tcommentCommonRepo comment_common.CommentCommonRepo,\n\tuserExternalLoginRepo user_external_login.UserExternalLoginRepo,\n\tnotificationRepo notificationcommon.NotificationRepo,\n\tpluginUserConfigRepo plugin_common.PluginUserConfigRepo,\n\tbadgeAwardRepo badge.BadgeAwardRepo,\n) *UserAdminService {\n\treturn &UserAdminService{\n\t\tuserRepo:              userRepo,\n\t\tuserRoleRelService:    userRoleRelService,\n\t\tauthService:           authService,\n\t\tuserCommonService:     userCommonService,\n\t\tuserActivity:          userActivity,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t\temailService:          emailService,\n\t\tquestionCommonRepo:    questionCommonRepo,\n\t\tanswerCommonRepo:      answerCommonRepo,\n\t\tcommentCommonRepo:     commentCommonRepo,\n\t\tuserExternalLoginRepo: userExternalLoginRepo,\n\t\tnotificationRepo:      notificationRepo,\n\t\tpluginUserConfigRepo:  pluginUserConfigRepo,\n\t\tbadgeAwardRepo:        badgeAwardRepo,\n\t}\n}\n\n// UpdateUserStatus update user\nfunc (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) {\n\t// Admin cannot modify their status\n\tif req.UserID == req.LoginUserID {\n\t\treturn errors.BadRequest(reason.AdminCannotModifySelfStatus)\n\t}\n\tuserInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\t// if user status is deleted\n\tif userInfo.Status == entity.UserStatusDeleted {\n\t\treturn nil\n\t}\n\n\tif req.IsInactive() {\n\t\tuserInfo.MailStatus = entity.EmailStatusToBeVerified\n\t}\n\tif req.IsDeleted() {\n\t\tuserInfo.Status = entity.UserStatusDeleted\n\t\tuserInfo.EMail = fmt.Sprintf(\"%s.%d\", userInfo.EMail, time.Now().Unix())\n\t}\n\tif req.IsSuspended() {\n\t\tuserInfo.Status = entity.UserStatusSuspended\n\t}\n\tif req.IsNormal() {\n\t\tuserInfo.Status = entity.UserStatusAvailable\n\t\tuserInfo.MailStatus = entity.EmailStatusAvailable\n\t}\n\n\tsuspendedUntil := req.GetSuspendedUntil()\n\terr = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail, suspendedUntil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// remove all content that user created, such as question, answer, comment, etc.\n\tif req.RemoveAllContent {\n\t\tus.removeAllUserCreatedContent(ctx, userInfo.ID)\n\t}\n\n\tif req.IsDeleted() {\n\t\tus.removeAllUserConfiguration(ctx, userInfo.ID)\n\t}\n\n\t// if user reputation is zero means this user is inactive, so try to activate this user.\n\tif req.IsNormal() && userInfo.Rank == 0 {\n\t\treturn us.userActivity.UserActive(ctx, userInfo.ID)\n\t}\n\treturn nil\n}\n\n// removeAllUserConfiguration remove all user configuration\nfunc (us *UserAdminService) removeAllUserConfiguration(ctx context.Context, userID string) {\n\terr := us.userExternalLoginRepo.DeleteUserExternalLoginByUserID(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"remove all user external login error: %v\", err)\n\t}\n\terr = us.notificationRepo.DeleteNotification(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"remove all user notification error: %v\", err)\n\t}\n\terr = us.notificationRepo.DeleteUserNotificationConfig(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"remove all user notification config error: %v\", err)\n\t}\n\terr = us.pluginUserConfigRepo.DeleteUserPluginConfig(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"remove all user plugin config error: %v\", err)\n\t}\n\terr = us.badgeAwardRepo.DeleteUserBadgeAward(ctx, userID)\n\tif err != nil {\n\t\tlog.Errorf(\"remove all user badge award error: %v\", err)\n\t}\n}\n\n// removeAllUserCreatedContent remove all user created content\nfunc (us *UserAdminService) removeAllUserCreatedContent(ctx context.Context, userID string) {\n\tif err := us.questionCommonRepo.RemoveAllUserQuestion(ctx, userID); err != nil {\n\t\tlog.Errorf(\"remove all user question error: %v\", err)\n\t}\n\tif err := us.answerCommonRepo.RemoveAllUserAnswer(ctx, userID); err != nil {\n\t\tlog.Errorf(\"remove all user answer error: %v\", err)\n\t}\n\tif err := us.commentCommonRepo.RemoveAllUserComment(ctx, userID); err != nil {\n\t\tlog.Errorf(\"remove all user comment error: %v\", err)\n\t}\n}\n\n// UpdateUserRole update user role\nfunc (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) {\n\t// Users cannot modify their roles\n\tif req.UserID == req.LoginUserID {\n\t\treturn errors.BadRequest(reason.UserCannotUpdateYourRole)\n\t}\n\n\terr = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tus.authService.RemoveUserAllTokens(ctx, req.UserID)\n\treturn\n}\n\n// AddUser add user\nfunc (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) {\n\t_, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif has {\n\t\treturn errors.BadRequest(reason.EmailDuplicate)\n\t}\n\n\thashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserInfo := &entity.User{}\n\tuserInfo.EMail = req.Email\n\tuserInfo.DisplayName = req.DisplayName\n\tuserInfo.Pass = string(hashPwd)\n\n\tuserInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserInfo.MailStatus = entity.EmailStatusAvailable\n\tuserInfo.Status = entity.UserStatusAvailable\n\tuserInfo.Rank = 1\n\n\terr = us.userRepo.AddUser(ctx, userInfo)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn\n}\n\n// AddUsers add users\nfunc (us *UserAdminService) AddUsers(ctx context.Context, req *schema.AddUsersReq) (\n\tresp []*validator.FormErrorField, err error) {\n\tresp, err = req.ParseUsers(ctx)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\terrData := us.checkUserDuplicateInner(ctx, req.Users)\n\tif errData != nil {\n\t\treturn errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError)\n\t}\n\tusers, errData, err := us.formatBulkAddUsers(ctx, req)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tif errData != nil {\n\t\treturn errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError)\n\t}\n\treturn nil, us.userRepo.AddUsers(ctx, users)\n}\n\nfunc (us *UserAdminService) checkUserDuplicateInner(ctx context.Context, users []*schema.AddUserReq) (\n\terrorData *schema.AddUsersErrorData) {\n\tlang := handler.GetLangByCtx(ctx)\n\tval := validator.GetValidatorByLang(lang)\n\n\temails := make(map[string]bool)\n\tdisplayNames := make(map[string]bool)\n\tfor line, user := range users {\n\t\tif errFields, e := val.Check(user); e != nil {\n\t\t\terrorData = &schema.AddUsersErrorData{}\n\t\t\tif len(errFields) > 0 {\n\t\t\t\terrorData.Field = errFields[0].ErrorField\n\t\t\t\terrorData.ExtraMessage = errFields[0].ErrorMsg\n\t\t\t}\n\t\t\terrorData.Line = line + 1\n\t\t\terrorData.Content = fmt.Sprintf(\"%s, %s, %s\", user.DisplayName, user.Email, user.Password)\n\t\t\treturn errorData\n\t\t}\n\t\tif emails[user.Email] {\n\t\t\treturn &schema.AddUsersErrorData{\n\t\t\t\tField:        \"email\",\n\t\t\t\tLine:         line + 1,\n\t\t\t\tContent:      user.Email,\n\t\t\t\tExtraMessage: translator.Tr(lang, reason.EmailDuplicate),\n\t\t\t}\n\t\t}\n\t\tif displayNames[user.DisplayName] {\n\t\t\treturn &schema.AddUsersErrorData{\n\t\t\t\tField:        \"name\",\n\t\t\t\tLine:         line + 1,\n\t\t\t\tContent:      user.DisplayName,\n\t\t\t\tExtraMessage: translator.Tr(lang, reason.UsernameDuplicate),\n\t\t\t}\n\t\t}\n\t\temails[user.Email] = true\n\t\tdisplayNames[user.DisplayName] = true\n\t}\n\treturn nil\n}\n\nfunc (us *UserAdminService) formatBulkAddUsers(ctx context.Context, req *schema.AddUsersReq) (\n\tusers []*entity.User, errorData *schema.AddUsersErrorData, err error) {\n\tlang := handler.GetLangByCtx(ctx)\n\terrorData = &schema.AddUsersErrorData{Line: -1}\n\tfor line, user := range req.Users {\n\t\t_, has, e := us.userRepo.GetUserInfoByEmail(ctx, user.Email)\n\t\tif e != nil {\n\t\t\treturn nil, nil, e\n\t\t}\n\t\tif has {\n\t\t\terrorData.Field = \"email\"\n\t\t\terrorData.Line = line + 1\n\t\t\terrorData.Content = user.Email\n\t\t\terrorData.ExtraMessage = translator.Tr(lang, reason.EmailDuplicate)\n\t\t\treturn nil, errorData, nil\n\t\t}\n\n\t\tuserInfo := &entity.User{}\n\t\tuserInfo.EMail = user.Email\n\t\tuserInfo.DisplayName = user.DisplayName\n\t\thashPwd, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)\n\t\tuserInfo.Pass = string(hashPwd)\n\t\tuserInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName)\n\t\tif err != nil {\n\t\t\terrorData.Field = \"name\"\n\t\t\terrorData.Line = line + 1\n\t\t\terrorData.Content = user.DisplayName\n\t\t\terrorData.ExtraMessage = translator.Tr(lang, reason.UsernameInvalid)\n\t\t\treturn nil, errorData, nil\n\t\t}\n\t\tuserInfo.MailStatus = entity.EmailStatusAvailable\n\t\tuserInfo.Status = entity.UserStatusAvailable\n\t\tuserInfo.Rank = 1\n\t\tusers = append(users, userInfo)\n\t}\n\treturn users, nil, nil\n}\n\n// UpdateUserPassword update user password\nfunc (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) {\n\t// Users cannot modify their password\n\tif req.UserID == req.LoginUserID {\n\t\treturn errors.BadRequest(reason.AdminCannotUpdateTheirPassword)\n\t}\n\tuserInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\n\thashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd))\n\tif err != nil {\n\t\treturn err\n\t}\n\t// logout this user\n\tus.authService.RemoveUserAllTokens(ctx, req.UserID)\n\treturn\n}\n\n// EditUserProfile edit user profile\nfunc (us *UserAdminService) EditUserProfile(ctx context.Context, req *schema.EditUserProfileReq) (\n\terrFields []*validator.FormErrorField, err error) {\n\tif req.UserID == req.LoginUserID {\n\t\treturn nil, errors.BadRequest(reason.AdminCannotEditTheirProfile)\n\t}\n\t_, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tif checker.IsInvalidUsername(req.Username) || checker.IsUsersIgnorePath(req.Username) {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"username\",\n\t\t\tErrorMsg:   reason.UsernameInvalid,\n\t\t}), errors.BadRequest(reason.UsernameInvalid)\n\t}\n\n\tuserInfo, exist, err := us.userCommonService.GetByUsername(ctx, req.Username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist && userInfo.ID != req.UserID {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"username\",\n\t\t\tErrorMsg:   reason.UsernameDuplicate,\n\t\t}), errors.BadRequest(reason.UsernameDuplicate)\n\t}\n\n\tuserInfo, exist, err = us.userCommonService.GetByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist && userInfo.ID != req.UserID {\n\t\treturn append(errFields, &validator.FormErrorField{\n\t\t\tErrorField: \"email\",\n\t\t\tErrorMsg:   reason.EmailDuplicate,\n\t\t}), errors.BadRequest(reason.EmailDuplicate)\n\t}\n\n\tuser := &entity.User{}\n\tuser.ID = req.UserID\n\tuser.DisplayName = req.DisplayName\n\tuser.Username = req.Username\n\tuser.EMail = req.Email\n\tuser.MailStatus = entity.EmailStatusAvailable\n\terr = us.userCommonService.UpdateUserProfile(ctx, user)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn\n}\n\n// GetUserInfo get user one\nfunc (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) {\n\tuser, exist, err := us.userRepo.GetUserInfo(ctx, userID)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tresp = &schema.GetUserInfoResp{}\n\t_ = copier.Copy(resp, user)\n\treturn resp, nil\n}\n\n// GetUserPage get user list page\nfunc (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) {\n\tuser := &entity.User{}\n\t_ = copier.Copy(user, req)\n\n\tswitch {\n\tcase req.IsInactive():\n\t\tuser.MailStatus = entity.EmailStatusToBeVerified\n\t\tuser.Status = entity.UserStatusAvailable\n\tcase req.IsSuspended():\n\t\tuser.Status = entity.UserStatusSuspended\n\tcase req.IsDeleted():\n\t\tuser.Status = entity.UserStatusDeleted\n\tdefault:\n\t\tuser.MailStatus = entity.EmailStatusAvailable\n\t\tuser.Status = entity.UserStatusAvailable\n\t}\n\n\tif len(req.Query) > 0 {\n\t\tif email, e := mail.ParseAddress(req.Query); e == nil {\n\t\t\tuser.EMail = email.Address\n\t\t\treq.Query = \"\"\n\t\t} else if after, ok := strings.CutPrefix(req.Query, \"user:\"); ok {\n\t\t\tid := strings.TrimSpace(after)\n\t\t\tidSearch := true\n\t\t\tfor _, r := range id {\n\t\t\t\tif !unicode.IsDigit(r) {\n\t\t\t\t\tidSearch = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif idSearch {\n\t\t\t\tuser.ID = id\n\t\t\t\treq.Query = \"\"\n\t\t\t} else {\n\t\t\t\treq.Query = id\n\t\t\t}\n\t\t}\n\t}\n\n\tusers, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query, req.Staff)\n\tif err != nil {\n\t\treturn\n\t}\n\tavatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, users)\n\n\tresp := make([]*schema.GetUserPageResp, 0)\n\tfor _, u := range users {\n\t\tt := &schema.GetUserPageResp{\n\t\t\tUserID:      u.ID,\n\t\t\tCreatedAt:   u.CreatedAt.Unix(),\n\t\t\tUsername:    u.Username,\n\t\t\tEMail:       u.EMail,\n\t\t\tRank:        u.Rank,\n\t\t\tDisplayName: u.DisplayName,\n\t\t\tAvatar:      avatarMapping[u.ID].GetURL(),\n\t\t}\n\t\tswitch {\n\t\tcase u.Status == entity.UserStatusDeleted:\n\t\t\tt.Status = constant.UserDeleted\n\t\t\tt.DeletedAt = u.DeletedAt.Unix()\n\t\tcase u.Status == entity.UserStatusSuspended:\n\t\t\tt.Status = constant.UserSuspended\n\t\t\tt.SuspendedAt = u.SuspendedAt.Unix()\n\t\t\tif !u.SuspendedUntil.IsZero() {\n\t\t\t\tt.SuspendedUntil = u.SuspendedUntil.Unix()\n\t\t\t}\n\t\tcase u.MailStatus == entity.EmailStatusToBeVerified:\n\t\t\tt.Status = constant.UserInactive\n\t\tdefault:\n\t\t\tt.Status = constant.UserNormal\n\t\t}\n\t\tresp = append(resp, t)\n\t}\n\tus.setUserRoleInfo(ctx, resp)\n\treturn pager.NewPageModel(total, resp), nil\n}\n\nfunc (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) {\n\tvar userIDs []string\n\tfor _, u := range resp {\n\t\tuserIDs = append(userIDs, u.UserID)\n\t}\n\n\tuserRoleMapping, err := us.userRoleRelService.GetUserRoleMapping(ctx, userIDs)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tfor _, u := range resp {\n\t\tr := userRoleMapping[u.UserID]\n\t\tif r == nil {\n\t\t\tcontinue\n\t\t}\n\t\tu.RoleID = r.ID\n\t\tu.RoleName = r.Name\n\t}\n}\n\nfunc (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) (\n\tresp *schema.GetUserActivationResp, err error) {\n\tuserInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tgeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  userInfo.EMail,\n\t\tUserID: userInfo.ID,\n\t}\n\tcode := token.GenerateToken()\n\tus.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString())\n\tresp = &schema.GetUserActivationResp{\n\t\tActivationURL: fmt.Sprintf(\"%s/users/account-activation?code=%s\", general.SiteUrl, code),\n\t}\n\treturn resp, nil\n}\n\n// SendUserActivation send user activation email\nfunc (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) {\n\tuserInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !exist {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\n\tgeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata := &schema.EmailCodeContent{\n\t\tEmail:  userInfo.EMail,\n\t\tUserID: userInfo.ID,\n\t}\n\tcode := token.GenerateToken()\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/account-activation?code=%s\", general.SiteUrl, code)\n\ttitle, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())\n\treturn nil\n}\n\nfunc (us *UserAdminService) DeletePermanently(ctx context.Context, req *schema.DeletePermanentlyReq) (err error) {\n\tswitch req.Type {\n\tcase constant.DeletePermanentlyUsers:\n\t\treturn us.userRepo.DeletePermanentlyUsers(ctx)\n\tcase constant.DeletePermanentlyQuestions:\n\t\treturn us.questionCommonRepo.DeletePermanentlyQuestions(ctx)\n\tcase constant.DeletePermanentlyAnswers:\n\t\treturn us.answerCommonRepo.DeletePermanentlyAnswers(ctx)\n\t}\n\n\treturn errors.BadRequest(reason.RequestFormatError)\n}\n\n// CheckAndUnsuspendExpiredUsers checks for users whose suspension has expired and restores them to normal status\nfunc (us *UserAdminService) CheckAndUnsuspendExpiredUsers(ctx context.Context) error {\n\t// Find all suspended users whose suspension time has expired\n\texpiredUsers, err := us.userRepo.GetExpiredSuspendedUsers(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnow := time.Now()\n\tfor _, user := range expiredUsers {\n\t\t// Check if suspension has expired (not permanent and time has passed)\n\t\tif user.Status == entity.UserStatusSuspended &&\n\t\t\t!user.SuspendedUntil.IsZero() &&\n\t\t\tuser.SuspendedUntil.Before(now) {\n\t\t\tlog.Infof(\"Unsuspending user %s (ID: %s) - suspension expired at %v\",\n\t\t\t\tuser.Username, user.ID, user.SuspendedUntil)\n\n\t\t\t// Update user status to normal\n\t\t\terr = us.userRepo.UpdateUserStatus(ctx, user.ID, entity.UserStatusAvailable,\n\t\t\t\tentity.EmailStatusAvailable, user.EMail, time.Time{})\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"Failed to unsuspend user %s (ID: %s): %v\",\n\t\t\t\t\tuser.Username, user.ID, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/service/user_common/user.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage usercommon\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/pkg/converter\"\n\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/auth\"\n\t\"github.com/apache/answer/internal/service/role\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/random\"\n\t\"github.com/mozillazg/go-pinyin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype UserRepo interface {\n\tAddUser(ctx context.Context, user *entity.User) (err error)\n\tIncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error)\n\tIncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error)\n\tUpdateQuestionCount(ctx context.Context, userID string, count int64) (err error)\n\tUpdateAnswerCount(ctx context.Context, userID string, count int) (err error)\n\tUpdateLastLoginDate(ctx context.Context, userID string) (err error)\n\tUpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error\n\tUpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error\n\tUpdateEmail(ctx context.Context, userID, email string) error\n\tUpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error)\n\tUpdatePass(ctx context.Context, userID, pass string) error\n\tUpdateInfo(ctx context.Context, userInfo *entity.User) (err error)\n\tUpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error)\n\tGetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)\n\tBatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)\n\tGetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error)\n\tGetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error)\n\tGetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)\n\tGetUserCount(ctx context.Context) (count int64, err error)\n\tSearchUserListByName(ctx context.Context, name string, limit int, onlyStaff bool) (userList []*entity.User, err error)\n\tIsAvatarFileUsed(ctx context.Context, filePath string) (bool, error)\n}\n\n// UserCommon user service\ntype UserCommon struct {\n\tuserRepo              UserRepo\n\tuserRoleService       *role.UserRoleRelService\n\tauthService           *auth.AuthService\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n}\n\nfunc NewUserCommon(\n\tuserRepo UserRepo,\n\tuserRoleService *role.UserRoleRelService,\n\tauthService *auth.AuthService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n) *UserCommon {\n\treturn &UserCommon{\n\t\tuserRepo:              userRepo,\n\t\tuserRoleService:       userRoleService,\n\t\tauthService:           authService,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t}\n}\n\nfunc (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, id string) (\n\tuserBasicInfo *schema.UserBasicInfo, exist bool, err error) {\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, id)\n\tif err != nil {\n\t\treturn nil, exist, err\n\t}\n\tinfo := us.FormatUserBasicInfo(ctx, userInfo)\n\tinfo.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\treturn info, exist, nil\n}\n\nfunc (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username string) (*schema.UserBasicInfo, bool, error) {\n\tuserInfo, exist, err := us.userRepo.GetByUsername(ctx, username)\n\tif err != nil {\n\t\treturn nil, exist, err\n\t}\n\tinfo := us.FormatUserBasicInfo(ctx, userInfo)\n\tinfo.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL()\n\treturn info, exist, nil\n}\n\nfunc (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) {\n\tinfomap := make(map[string]*schema.UserBasicInfo)\n\tlist, err := us.userRepo.GetByUsernames(ctx, usernames)\n\tif err != nil {\n\t\treturn infomap, err\n\t}\n\tavatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, list)\n\tfor _, user := range list {\n\t\tinfo := us.FormatUserBasicInfo(ctx, user)\n\t\tinfo.Avatar = avatarMapping[user.ID].GetURL()\n\t\tinfomap[user.Username] = info\n\t}\n\treturn infomap, nil\n}\n\nfunc (us *UserCommon) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {\n\treturn us.userRepo.GetByEmail(ctx, email)\n}\n\nfunc (us *UserCommon) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) {\n\treturn us.userRepo.GetByUsername(ctx, username)\n}\n\nfunc (us *UserCommon) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) {\n\treturn us.userRepo.UpdateUserProfile(ctx, userInfo)\n}\n\nfunc (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {\n\treturn us.userRepo.UpdateAnswerCount(ctx, userID, num)\n}\n\nfunc (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int64) error {\n\treturn us.userRepo.UpdateQuestionCount(ctx, userID, num)\n}\n\nfunc (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, userIDs []string) (map[string]*schema.UserBasicInfo, error) {\n\tuserIDs = checker.FilterEmptyString(userIDs)\n\tuserMap := make(map[string]*schema.UserBasicInfo)\n\tif len(userIDs) == 0 {\n\t\treturn userMap, nil\n\t}\n\tuserList, err := us.userRepo.BatchGetByID(ctx, userIDs)\n\tif err != nil {\n\t\treturn userMap, err\n\t}\n\tavatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, userList)\n\tfor _, user := range userList {\n\t\tinfo := us.FormatUserBasicInfo(ctx, user)\n\t\tinfo.Avatar = avatarMapping[user.ID].GetURL()\n\t\tuserMap[user.ID] = info\n\t}\n\tfor _, id := range userIDs {\n\t\tif _, ok := userMap[id]; !ok {\n\t\t\tuserMap[id] = &schema.UserBasicInfo{\n\t\t\t\tID:          id,\n\t\t\t\tDisplayName: \"user\" + converter.DeleteUserDisplay(id),\n\t\t\t\tStatus:      constant.UserDeleted,\n\t\t\t}\n\t\t}\n\t}\n\treturn userMap, nil\n}\n\n// FormatUserBasicInfo format user basic info\nfunc (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo {\n\tuserBasicInfo := &schema.UserBasicInfo{}\n\tuserBasicInfo.ID = userInfo.ID\n\tuserBasicInfo.Username = userInfo.Username\n\tuserBasicInfo.Rank = userInfo.Rank\n\tuserBasicInfo.DisplayName = userInfo.DisplayName\n\tuserBasicInfo.Website = userInfo.Website\n\tuserBasicInfo.Location = userInfo.Location\n\tuserBasicInfo.Language = userInfo.Language\n\tuserBasicInfo.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)\n\tif !userInfo.SuspendedUntil.IsZero() {\n\t\tuserBasicInfo.SuspendedUntil = userInfo.SuspendedUntil.Unix()\n\t}\n\tif userBasicInfo.Status == constant.UserDeleted {\n\t\tuserBasicInfo.Avatar = \"\"\n\t\tuserBasicInfo.DisplayName = \"user\" + converter.DeleteUserDisplay(userInfo.ID)\n\t}\n\treturn userBasicInfo\n}\n\n// MakeUsername\n// Generate a unique Username based on the displayName\nfunc (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (username string, err error) {\n\t// Chinese processing\n\tif has := checker.IsChinese(displayName); has {\n\t\tdisplayName = strings.Join(pinyin.LazyConvert(displayName, nil), \"\")\n\t}\n\n\tusername = strings.ReplaceAll(displayName, \" \", \"-\")\n\tusername = strings.ToLower(username)\n\tsuffix := \"\"\n\n\tif checker.IsInvalidUsername(username) {\n\t\treturn \"\", errors.BadRequest(reason.UsernameInvalid)\n\t}\n\n\tif checker.IsReservedUsername(username) {\n\t\treturn \"\", errors.BadRequest(reason.UsernameInvalid)\n\t}\n\n\tfor {\n\t\t_, has, err := us.userRepo.GetByUsername(ctx, username+suffix)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif !has {\n\t\t\tbreak\n\t\t}\n\t\tsuffix = random.UsernameSuffix()\n\t}\n\treturn username + suffix, nil\n}\n\nfunc (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) (\n\taccessToken string, userCacheInfo *entity.UserCacheInfo, err error) {\n\troleID, err := us.userRoleService.GetUserRole(ctx, userID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tuserCacheInfo = &entity.UserCacheInfo{\n\t\tUserID:      userID,\n\t\tEmailStatus: emailStatus,\n\t\tUserStatus:  userStatus,\n\t\tRoleID:      roleID,\n\t\tExternalID:  externalID,\n\t}\n\n\taccessToken, _, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tif userCacheInfo.RoleID == role.RoleAdminID {\n\t\tif err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, userCacheInfo); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t}\n\treturn accessToken, userCacheInfo, nil\n}\n\nfunc (us *UserCommon) IsAvatarFileUsed(ctx context.Context, filePath string) bool {\n\tused, err := us.userRepo.IsAvatarFileUsed(ctx, filePath)\n\tif err != nil {\n\t\tlog.Errorf(\"error checking if branding file is used: %v\", err)\n\t\t// will try again with the next clean up\n\t\treturn true\n\t}\n\treturn used\n}\n"
  },
  {
    "path": "internal/service/user_external_login/user_center_login_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_external_login\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/apache/answer/pkg/random\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\n// UserCenterLoginService user external login service\ntype UserCenterLoginService struct {\n\tuserRepo              usercommon.UserRepo\n\tuserExternalLoginRepo UserExternalLoginRepo\n\tuserCommonService     *usercommon.UserCommon\n\tuserActivity          activity.UserActiveActivityRepo\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService\n}\n\n// NewUserCenterLoginService new user external login service\nfunc NewUserCenterLoginService(\n\tuserRepo usercommon.UserRepo,\n\tuserCommonService *usercommon.UserCommon,\n\tuserExternalLoginRepo UserExternalLoginRepo,\n\tuserActivity activity.UserActiveActivityRepo,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n) *UserCenterLoginService {\n\treturn &UserCenterLoginService{\n\t\tuserRepo:              userRepo,\n\t\tuserCommonService:     userCommonService,\n\t\tuserExternalLoginRepo: userExternalLoginRepo,\n\t\tuserActivity:          userActivity,\n\t\tsiteInfoCommonService: siteInfoCommonService,\n\t}\n}\n\nfunc (us *UserCenterLoginService) ExternalLogin(\n\tctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) (\n\tresp *schema.UserExternalLoginResp, err error) {\n\tif len(basicUserInfo.ExternalID) == 0 {\n\t\treturn &schema.UserExternalLoginResp{\n\t\t\tErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),\n\t\t\tErrMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID),\n\t\t}, nil\n\t}\n\n\tif len(basicUserInfo.Email) > 0 {\n\t\t// check whether site allow register or not\n\t\tsiteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) {\n\t\t\tlog.Debugf(\"email domain not allowed: %s\", basicUserInfo.Email)\n\t\t\treturn &schema.UserExternalLoginResp{\n\t\t\t\tErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),\n\t\t\t\tErrMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),\n\t\t\t}, nil\n\t\t}\n\t}\n\n\toldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,\n\t\tuserCenter.Info().SlugName, basicUserInfo.ExternalID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\t// if user is already a member, login directly\n\t\toldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist {\n\t\t\t// if user is deleted, do not allow login\n\t\t\tif oldUserInfo.Status == entity.UserStatusDeleted {\n\t\t\t\treturn &schema.UserExternalLoginResp{\n\t\t\t\t\tErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),\n\t\t\t\t\tErrMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied),\n\t\t\t\t}, nil\n\t\t\t}\n\t\t\tif err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {\n\t\t\t\tlog.Errorf(\"update user last login date failed: %v\", err)\n\t\t\t}\n\t\t\taccessToken, _, err := us.userCommonService.CacheLoginUserInfo(\n\t\t\t\tctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)\n\t\t\treturn &schema.UserExternalLoginResp{AccessToken: accessToken}, err\n\t\t}\n\t}\n\n\t// cache external user info, waiting for user enter email address.\n\tif userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 {\n\t\treturn &schema.UserExternalLoginResp{ErrMsg: \"Requires authorized email to login\"}, nil\n\t}\n\n\toldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := us.activeUser(ctx, oldUserInfo); err != nil {\n\t\treturn nil, err\n\t}\n\n\taccessToken, _, err := us.userCommonService.CacheLoginUserInfo(\n\t\tctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)\n\treturn &schema.UserExternalLoginResp{AccessToken: accessToken}, err\n}\n\nfunc (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string,\n\tbasicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) {\n\tuserInfo = &entity.User{}\n\tuserInfo.EMail = basicUserInfo.Email\n\tuserInfo.DisplayName = basicUserInfo.DisplayName\n\n\tuserInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tuserInfo.Username = random.Username()\n\t}\n\n\tif len(basicUserInfo.Avatar) > 0 {\n\t\tavatarInfo := &schema.AvatarInfo{\n\t\t\tType:   constant.AvatarTypeCustom,\n\t\t\tCustom: basicUserInfo.Avatar,\n\t\t}\n\t\tavatar, _ := json.Marshal(avatarInfo)\n\t\tuserInfo.Avatar = string(avatar)\n\t}\n\n\tuserInfo.MailStatus = entity.EmailStatusAvailable\n\tuserInfo.Status = entity.UserStatusAvailable\n\tuserInfo.LastLoginDate = time.Now()\n\tuserInfo.Bio = basicUserInfo.Bio\n\tuserInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio)\n\terr = us.userRepo.AddUser(ctx, userInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetaInfo, _ := json.Marshal(basicUserInfo)\n\tnewExternalUserInfo := &entity.UserExternalLogin{\n\t\tUserID:     userInfo.ID,\n\t\tProvider:   provider,\n\t\tExternalID: basicUserInfo.ExternalID,\n\t\tMetaInfo:   string(metaInfo),\n\t}\n\terr = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn userInfo, nil\n}\n\nfunc (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) error {\n\tif err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {\n\t\tlog.Error(err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) (\n\tresp *schema.UserCenterUserSettingsResp, err error) {\n\tresp = &schema.UserCenterUserSettingsResp{}\n\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn resp, nil\n\t}\n\n\t// get external login info\n\texternalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar externalInfo *entity.UserExternalLogin\n\tfor _, t := range externalLoginList {\n\t\tif t.Provider == userCenter.Info().SlugName {\n\t\t\texternalInfo = t\n\t\t}\n\t}\n\tif externalInfo == nil {\n\t\treturn resp, nil\n\t}\n\n\tsettings, err := userCenter.UserSettings(externalInfo.ExternalID)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn resp, nil\n\t}\n\n\tif len(settings.AccountSettingRedirectURL) > 0 {\n\t\tresp.AccountSettingAgent = schema.UserSettingAgent{\n\t\t\tEnabled:     true,\n\t\t\tRedirectURL: settings.AccountSettingRedirectURL,\n\t\t}\n\t}\n\tif len(settings.ProfileSettingRedirectURL) > 0 {\n\t\tresp.ProfileSettingAgent = schema.UserSettingAgent{\n\t\t\tEnabled:     true,\n\t\t\tRedirectURL: settings.ProfileSettingRedirectURL,\n\t\t}\n\t}\n\treturn resp, nil\n}\n\n// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions\n// are turned off due to turning on the User Center plugin.\nfunc (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) (\n\tresp *schema.UserCenterAdminFunctionAgentResp, err error) {\n\tresp = &schema.UserCenterAdminFunctionAgentResp{\n\t\tAllowCreateUser:         true,\n\t\tAllowUpdateUserStatus:   true,\n\t\tAllowUpdateUserPassword: true,\n\t\tAllowUpdateUserRole:     true,\n\t}\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn\n\t}\n\tdesc := userCenter.Description()\n\t// If user status agent is enabled, admin can not update user status in answer.\n\tresp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled\n\tresp.AllowUpdateUserRole = !desc.UserRoleAgentEnabled\n\n\t// If original user system is enabled, admin can update user password and role in answer.\n\tresp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem\n\tresp.AllowCreateUser = desc.EnabledOriginalUserSystem\n\treturn resp, nil\n}\n\nfunc (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) (\n\tresp *schema.UserCenterPersonalBranding, err error) {\n\tresp = &schema.UserCenterPersonalBranding{\n\t\tPersonalBranding: make([]*schema.PersonalBranding, 0),\n\t}\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn\n\t}\n\n\tuserInfo, exist, err := us.userRepo.GetByUsername(ctx, username)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn resp, nil\n\t}\n\n\t// get external login info\n\texternalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar externalInfo *entity.UserExternalLogin\n\tfor _, t := range externalLoginList {\n\t\tif t.Provider == userCenter.Info().SlugName {\n\t\t\texternalInfo = t\n\t\t}\n\t}\n\tif externalInfo == nil {\n\t\treturn resp, nil\n\t}\n\n\tresp.Enabled = true\n\tbranding := userCenter.PersonalBranding(externalInfo.ExternalID)\n\n\tfor _, t := range branding {\n\t\tresp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{\n\t\t\tIcon:  t.Icon,\n\t\t\tName:  t.Name,\n\t\t\tLabel: t.Label,\n\t\t\tUrl:   t.Url,\n\t\t})\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/service/user_external_login/user_external_login_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_external_login\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\t\"github.com/apache/answer/internal/service/activity\"\n\t\"github.com/apache/answer/internal/service/export\"\n\t\"github.com/apache/answer/internal/service/siteinfo_common\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n\t\"github.com/apache/answer/internal/service/user_notification_config\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/random\"\n\t\"github.com/apache/answer/pkg/token\"\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/errors\"\n\t\"github.com/segmentfault/pacman/log\"\n)\n\ntype UserExternalLoginRepo interface {\n\tAddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error)\n\tUpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)\n\tGetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)\n\tGetByUserID(ctx context.Context, provider, userID string) (userInfo *entity.UserExternalLogin, exist bool, err error)\n\tGetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)\n\tDeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error)\n\tDeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error)\n\tSetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error)\n\tGetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error)\n}\n\n// UserExternalLoginService user external login service\ntype UserExternalLoginService struct {\n\tuserRepo                      usercommon.UserRepo\n\tuserExternalLoginRepo         UserExternalLoginRepo\n\tuserCommonService             *usercommon.UserCommon\n\temailService                  *export.EmailService\n\tsiteInfoCommonService         siteinfo_common.SiteInfoCommonService\n\tuserActivity                  activity.UserActiveActivityRepo\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService\n}\n\n// NewUserExternalLoginService new user external login service\nfunc NewUserExternalLoginService(\n\tuserRepo usercommon.UserRepo,\n\tuserCommonService *usercommon.UserCommon,\n\tuserExternalLoginRepo UserExternalLoginRepo,\n\temailService *export.EmailService,\n\tsiteInfoCommonService siteinfo_common.SiteInfoCommonService,\n\tuserActivity activity.UserActiveActivityRepo,\n\tuserNotificationConfigService *user_notification_config.UserNotificationConfigService,\n) *UserExternalLoginService {\n\treturn &UserExternalLoginService{\n\t\tuserRepo:                      userRepo,\n\t\tuserCommonService:             userCommonService,\n\t\tuserExternalLoginRepo:         userExternalLoginRepo,\n\t\temailService:                  emailService,\n\t\tsiteInfoCommonService:         siteInfoCommonService,\n\t\tuserActivity:                  userActivity,\n\t\tuserNotificationConfigService: userNotificationConfigService,\n\t}\n}\n\n// ExternalLogin if user is already a member logged in\nfunc (us *UserExternalLoginService) ExternalLogin(\n\tctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (\n\tresp *schema.UserExternalLoginResp, err error) {\n\tif len(externalUserInfo.ExternalID) == 0 {\n\t\treturn &schema.UserExternalLoginResp{\n\t\t\tErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),\n\t\t\tErrMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID),\n\t\t}, nil\n\t}\n\n\toldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,\n\t\texternalUserInfo.Provider, externalUserInfo.ExternalID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\t// if user is already a member, login directly\n\t\toldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif exist && oldUserInfo.Status != entity.UserStatusDeleted {\n\t\t\tif err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {\n\t\t\t\tlog.Errorf(\"update user last login date failed: %v\", err)\n\t\t\t}\n\t\t\tnewMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\taccessToken, _, err := us.userCommonService.CacheLoginUserInfo(\n\t\t\t\tctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)\n\t\t\treturn &schema.UserExternalLoginResp{AccessToken: accessToken}, err\n\t\t}\n\t}\n\n\t// cache external user info, waiting for user enter email address.\n\tif len(externalUserInfo.Email) == 0 {\n\t\tbindingKey := token.GenerateToken()\n\t\terr = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil\n\t}\n\n\t// check whether site allow register or not\n\tsiteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !checker.EmailInAllowEmailDomain(externalUserInfo.Email, siteInfo.AllowEmailDomains) {\n\t\tlog.Debugf(\"email domain not allowed: %s\", externalUserInfo.Email)\n\t\treturn &schema.UserExternalLoginResp{\n\t\t\tErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),\n\t\t\tErrMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),\n\t\t}, nil\n\t}\n\n\toldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// if user is not a member, register a new user\n\tif !exist {\n\t\toldUserInfo, err = us.registerNewUser(ctx, externalUserInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// bind external user info to user\n\terr = us.bindOldUser(ctx, externalUserInfo, oldUserInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If user login with external account and email is exist, active user directly.\n\tnewMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\n\t// set default user notification config for external user\n\tif err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{oldUserInfo.ID}); err != nil {\n\t\tlog.Errorf(\"set default user notification config failed, err: %v\", err)\n\t}\n\n\taccessToken, _, err := us.userCommonService.CacheLoginUserInfo(\n\t\tctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)\n\treturn &schema.UserExternalLoginResp{AccessToken: accessToken}, err\n}\n\nfunc (us *UserExternalLoginService) registerNewUser(ctx context.Context,\n\texternalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) {\n\tuserInfo = &entity.User{}\n\tuserInfo.EMail = externalUserInfo.Email\n\tuserInfo.DisplayName = externalUserInfo.DisplayName\n\n\tuserInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tuserInfo.Username = random.Username()\n\t}\n\n\tif len(externalUserInfo.Avatar) > 0 {\n\t\tavatarInfo := &schema.AvatarInfo{\n\t\t\tType:   constant.AvatarTypeCustom,\n\t\t\tCustom: externalUserInfo.Avatar,\n\t\t}\n\t\tavatar, _ := json.Marshal(avatarInfo)\n\t\tuserInfo.Avatar = string(avatar)\n\t}\n\n\tuserInfo.MailStatus = entity.EmailStatusToBeVerified\n\tuserInfo.Status = entity.UserStatusAvailable\n\tuserInfo.LastLoginDate = time.Now()\n\tuserInfo.Bio = externalUserInfo.Bio\n\tuserInfo.BioHTML = externalUserInfo.Bio\n\terr = us.userRepo.AddUser(ctx, userInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn userInfo, nil\n}\n\nfunc (us *UserExternalLoginService) bindOldUser(ctx context.Context,\n\texternalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) {\n\toldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,\n\t\texternalUserInfo.Provider,\n\t\texternalUserInfo.ExternalID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif exist {\n\t\toldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo\n\t\toldExternalUserInfo.UserID = oldUserInfo.ID\n\t\terr = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo)\n\t} else {\n\t\tnewExternalUserInfo := &entity.UserExternalLogin{\n\t\t\tUserID:     oldUserInfo.ID,\n\t\t\tProvider:   externalUserInfo.Provider,\n\t\t\tExternalID: externalUserInfo.ExternalID,\n\t\t\tMetaInfo:   externalUserInfo.MetaInfo,\n\t\t}\n\t\terr = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)\n\t}\n\treturn err\n}\n\nfunc (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User,\n\texternalUserInfo *schema.ExternalLoginUserInfoCache) (\n\tmailStatus int, err error) {\n\tlog.Infof(\"user %s login with external account, try to active email, old status is %d\",\n\t\toldUserInfo.ID, oldUserInfo.MailStatus)\n\n\t// try to active user email\n\tif oldUserInfo.MailStatus == entity.EmailStatusToBeVerified {\n\t\terr = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable)\n\t\tif err != nil {\n\t\t\treturn oldUserInfo.MailStatus, err\n\t\t}\n\t}\n\n\t// try to update user avatar\n\tif oldUserInfo.Avatar == \"\" && len(externalUserInfo.Avatar) > 0 {\n\t\tavatarInfo := &schema.AvatarInfo{\n\t\t\tType:   constant.AvatarTypeCustom,\n\t\t\tCustom: externalUserInfo.Avatar,\n\t\t}\n\t\tavatar, _ := json.Marshal(avatarInfo)\n\t\toldUserInfo.Avatar = string(avatar)\n\t\terr = us.userRepo.UpdateInfo(ctx, oldUserInfo)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\n\tif err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {\n\t\treturn oldUserInfo.MailStatus, err\n\t}\n\treturn entity.EmailStatusAvailable, nil\n}\n\n// ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user\nfunc (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(\n\tctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) (\n\tresp *schema.ExternalLoginBindingUserSendEmailResp, err error) {\n\tsiteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &schema.ExternalLoginBindingUserSendEmailResp{}\n\texternalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey)\n\tif err != nil || externalLoginInfo == nil {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tif len(externalLoginInfo.Email) > 0 {\n\t\tlog.Warnf(\"the binding email has been sent %s\", req.BindingKey)\n\t\treturn &schema.ExternalLoginBindingUserSendEmailResp{}, nil\n\t}\n\n\tuserInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist && !req.Must {\n\t\tresp.EmailExistAndMustBeConfirmed = true\n\t\treturn resp, nil\n\t}\n\n\tif !exist {\n\t\texternalLoginInfo.Email = req.Email\n\t\tuserInfo, err = us.registerNewUser(ctx, externalLoginInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo(\n\t\t\tctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n\terr = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// send bind confirmation email\n\tdata := &schema.EmailCodeContent{\n\t\tSourceType: schema.BindingSourceType,\n\t\tEmail:      req.Email,\n\t\tUserID:     userInfo.ID,\n\t\tBindingKey: req.BindingKey,\n\t}\n\tcode := token.GenerateToken()\n\tverifyEmailURL := fmt.Sprintf(\"%s/users/account-activation?code=%s\", siteGeneral.SiteUrl, code)\n\ttitle, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgo us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())\n\treturn resp, nil\n}\n\n// ExternalLoginBindingUser\n// The user clicks on the email link of the bound account and requests the API to bind the user officially\nfunc (us *UserExternalLoginService) ExternalLoginBindingUser(\n\tctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) {\n\texternalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey)\n\tif err != nil || externalLoginInfo == nil {\n\t\treturn errors.BadRequest(reason.UserNotFound)\n\t}\n\treturn us.bindOldUser(ctx, externalLoginInfo, oldUserInfo)\n}\n\n// GetExternalLoginUserInfoList get external login user info list\nfunc (us *UserExternalLoginService) GetExternalLoginUserInfoList(\n\tctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) {\n\treturn us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)\n}\n\n// ExternalLoginUnbinding external login unbinding\nfunc (us *UserExternalLoginService) ExternalLoginUnbinding(\n\tctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) {\n\t// If user has only one external login and never set password, he can't unbind it.\n\tuserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !exist {\n\t\treturn nil, errors.BadRequest(reason.UserNotFound)\n\t}\n\tif len(userInfo.Pass) == 0 {\n\t\tloginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(loginList) <= 1 {\n\t\t\treturn schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden)\n\t\t}\n\t}\n\n\treturn nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID)\n}\n\n// CheckUserStatusInUserCenter check user status in user center\nfunc (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) (\n\tvalid bool, externalID string, err error) {\n\t// If enable user center plugin, user status should be checked by user center\n\tuserCenter, ok := plugin.GetUserCenter()\n\tif !ok {\n\t\treturn true, \"\", nil\n\t}\n\tuserInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\tvar thisUcUserInfo *entity.UserExternalLogin\n\tfor _, t := range userInfoList {\n\t\tif t.Provider == userCenter.Info().SlugName {\n\t\t\tthisUcUserInfo = t\n\t\t\tbreak\n\t\t}\n\t}\n\t// If this user not login by user center, no need to check user status\n\tif thisUcUserInfo == nil {\n\t\treturn true, \"\", nil\n\t}\n\tuserStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID)\n\tif userStatus == plugin.UserStatusDeleted {\n\t\treturn false, thisUcUserInfo.ExternalID, nil\n\t}\n\treturn true, thisUcUserInfo.ExternalID, nil\n}\n"
  },
  {
    "path": "internal/service/user_notification_config/user_notification_config_service.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage user_notification_config\n\nimport (\n\t\"context\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/apache/answer/internal/schema\"\n\tusercommon \"github.com/apache/answer/internal/service/user_common\"\n)\n\ntype UserNotificationConfigRepo interface {\n\tAdd(ctx context.Context, userIDs []string, source, channels string) (err error)\n\tSave(ctx context.Context, uc *entity.UserNotificationConfig) (err error)\n\tGetByUserID(ctx context.Context, userID string) ([]*entity.UserNotificationConfig, error)\n\tGetBySource(ctx context.Context, source constant.NotificationSource) ([]*entity.UserNotificationConfig, error)\n\tGetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) (\n\t\tconf *entity.UserNotificationConfig, exist bool, err error)\n\tGetByUsersAndSource(ctx context.Context, userIDs []string, source constant.NotificationSource) (\n\t\t[]*entity.UserNotificationConfig, error)\n}\n\ntype UserNotificationConfigService struct {\n\tuserRepo                   usercommon.UserRepo\n\tuserNotificationConfigRepo UserNotificationConfigRepo\n}\n\nfunc NewUserNotificationConfigService(\n\tuserRepo usercommon.UserRepo,\n\tuserNotificationConfigRepo UserNotificationConfigRepo,\n) *UserNotificationConfigService {\n\treturn &UserNotificationConfigService{\n\t\tuserRepo:                   userRepo,\n\t\tuserNotificationConfigRepo: userNotificationConfigRepo,\n\t}\n}\n\nfunc (us *UserNotificationConfigService) GetUserNotificationConfig(ctx context.Context, userID string) (\n\tresp *schema.GetUserNotificationConfigResp, err error) {\n\tnotificationConfigs, err := us.userNotificationConfigRepo.GetByUserID(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &schema.GetUserNotificationConfigResp{}\n\tresp.NotificationConfig = schema.NewNotificationConfig(notificationConfigs)\n\tresp.Format()\n\treturn resp, nil\n}\n\nfunc (us *UserNotificationConfigService) UpdateUserNotificationConfig(\n\tctx context.Context, req *schema.UpdateUserNotificationConfigReq) (err error) {\n\treq.Format()\n\n\terr = us.userNotificationConfigRepo.Save(ctx,\n\t\tus.convertToEntity(ctx, req.UserID, constant.InboxSource, req.Inbox))\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = us.userNotificationConfigRepo.Save(ctx,\n\t\tus.convertToEntity(ctx, req.UserID, constant.AllNewQuestionSource, req.AllNewQuestion))\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = us.userNotificationConfigRepo.Save(ctx,\n\t\tus.convertToEntity(ctx, req.UserID, constant.AllNewQuestionForFollowingTagsSource,\n\t\t\treq.AllNewQuestionForFollowingTags))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// SetDefaultUserNotificationConfig set default user notification config for user register\nfunc (us *UserNotificationConfigService) SetDefaultUserNotificationConfig(ctx context.Context, userIDs []string) (\n\terr error) {\n\treturn us.userNotificationConfigRepo.Add(ctx, userIDs,\n\t\tstring(constant.InboxSource), `[{\"key\":\"email\",\"enable\":true}]`)\n}\n\nfunc (us *UserNotificationConfigService) convertToEntity(_ context.Context, userID string,\n\tsource constant.NotificationSource, channel schema.NotificationChannelConfig) (c *entity.UserNotificationConfig) {\n\tvar channels schema.NotificationChannels\n\tchannels = append(channels, &channel)\n\tc = &entity.UserNotificationConfig{\n\t\tUserID:   userID,\n\t\tSource:   string(source),\n\t\tChannels: channels.ToJsonString(),\n\t}\n\tfor _, ch := range channels {\n\t\tif ch.Enable {\n\t\t\tc.Enabled = true\n\t\t\tbreak\n\t\t}\n\t}\n\treturn c\n}\n"
  },
  {
    "path": "licenserc.toml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nheaderPath = \"Apache-2.0-ASF.txt\"\n\nexcludes = [\n  \"docs/release/**\",\n  \"ui/build/**\",\n  \"answer-data/**\",\n  \"NOTICE\",\n  \"DISCLAIMER\",\n  \"Makefile\",\n  \"go.mod\",\n  \"go.sum\",\n  \"ui/.eslintignore\",\n  \"ui/.browserslistrc\",\n  \"ui/.npmrc\",\n  \"ui/.env.*\",\n  \"script/plugin_list\",\n  \"charts/templates/_helpers.tpl\",\n  \"charts/.helmignore\",\n]\n"
  },
  {
    "path": "pkg/checker/chinese.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport \"unicode\"\n\nfunc IsChinese(str string) bool {\n\tfor _, v := range str {\n\t\tif unicode.Is(unicode.Han, v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/checker/email.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport \"strings\"\n\nfunc EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool {\n\tif len(allowEmailDomains) == 0 {\n\t\treturn true\n\t}\n\n\tfor _, domain := range allowEmailDomains {\n\t\tif strings.HasSuffix(email, domain) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/checker/file_type.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t_ \"image/gif\" // use init to support decode jpeg,jpg,png,gif\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/segmentfault/pacman/log\"\n\t\"golang.org/x/image/webp\"\n)\n\n// IsUnAuthorizedExtension check whether the file extension is not in the allowedExtensions\n// WANING Only checks the file extension is not reliable, but `http.DetectContentType` and `mimetype` are not reliable for all file types.\nfunc IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool {\n\text := strings.ToLower(strings.Trim(filepath.Ext(fileName), \".\"))\n\treturn !slices.Contains(allowedExtensions, ext)\n}\n\n// DecodeAndCheckImageFile currently answers support image type is\n// `image/jpeg, image/jpg, image/png, image/gif, image/webp`\nfunc DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool {\n\text := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), \".\"))\n\tswitch ext {\n\tcase \"jpg\", \"jpeg\", \"png\", \"gif\": // only allow for `image/jpeg, image/jpg, image/png, image/gif`\n\t\tif !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageConfigCheck) {\n\t\t\treturn false\n\t\t}\n\t\tif !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageCheck) {\n\t\t\treturn false\n\t\t}\n\tcase \"webp\":\n\t\tif !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageConfigCheck) {\n\t\t\treturn false\n\t\t}\n\t\tif !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageCheck) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checker func(file io.Reader, maxImageMegapixel int) error) bool {\n\tfile, err := os.Open(localFilePath)\n\tif err != nil {\n\t\tlog.Errorf(\"open file error: %v\", err)\n\t\treturn false\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\n\tif err = checker(file, maxImageMegapixel); err != nil {\n\t\tlog.Errorf(\"check image format error: %v\", err)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error {\n\tconfig, _, err := image.DecodeConfig(file)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decode image config error: %v\", err)\n\t}\n\tif imageSizeTooLarge(config, maxImageMegapixel) {\n\t\treturn fmt.Errorf(\"image size too large\")\n\t}\n\treturn nil\n}\n\nfunc standardImageCheck(file io.Reader, maxImageMegapixel int) error {\n\t_, _, err := image.Decode(file)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decode image error: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error {\n\tconfig, err := webp.DecodeConfig(file)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decode webp image config error: %v\", err)\n\t}\n\tif imageSizeTooLarge(config, maxImageMegapixel) {\n\t\treturn fmt.Errorf(\"image size too large\")\n\t}\n\treturn nil\n}\n\nfunc webpImageCheck(file io.Reader, maxImageMegapixel int) error {\n\t_, err := webp.Decode(file)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decode webp image error: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc imageSizeTooLarge(config image.Config, maxImageMegapixel int) bool {\n\treturn config.Width*config.Height > maxImageMegapixel\n}\n"
  },
  {
    "path": "pkg/checker/password.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nconst (\n\tlevelD = iota\n\tLevelC\n\tLevelB\n\tLevelA\n\tLevelS\n)\n\nconst (\n\tPasswordCannotContainSpaces = \"error.password.space_invalid\"\n)\n\n// CheckPassword checks the password strength\nfunc CheckPassword(password string) error {\n\tif strings.Contains(password, \" \") {\n\t\treturn errors.New(PasswordCannotContainSpaces)\n\t}\n\n\t// TODO Currently there is no requirement for password strength\n\tminLevel := 0\n\n\t// The password strength level is initialized to D.\n\t// The regular is used to verify the password strength.\n\t// If the matching is successful, the password strength increases by 1\n\tlevel := levelD\n\tpatternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`}\n\tfor _, pattern := range patternList {\n\t\tmatch, _ := regexp.MatchString(pattern, password)\n\t\tif match {\n\t\t\tlevel++\n\t\t}\n\t}\n\n\t// If the final password strength falls below the required minimum strength, return with an error\n\tif level < minLevel {\n\t\treturn fmt.Errorf(\"the password does not satisfy the current policy requirements\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/checker/path_ignore.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/apache/answer/configs\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype PathIgnore struct {\n\tUsers     []string `yaml:\"users\"`\n\tQuestions []string `yaml:\"questions\"`\n}\n\nvar (\n\tignorePathInit sync.Once\n\tpathIgnore     = &PathIgnore{}\n)\n\nfunc initPathIgnore() {\n\tif err := yaml.Unmarshal(configs.PathIgnore, pathIgnore); err != nil {\n\t\tlog.Error(err)\n\t}\n}\n\n// IsUsersIgnorePath checks whether the username is in ignore path\nfunc IsUsersIgnorePath(username string) bool {\n\tignorePathInit.Do(initPathIgnore)\n\treturn slices.Contains(pathIgnore.Users, username)\n}\n\n// IsQuestionsIgnorePath checks whether the questionID is in ignore path\nfunc IsQuestionsIgnorePath(questionID string) bool {\n\tignorePathInit.Do(initPathIgnore)\n\treturn slices.Contains(pathIgnore.Questions, questionID)\n}\n"
  },
  {
    "path": "pkg/checker/question_link.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/pkg/obj\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\nconst (\n\tQuestionLinkTypeURL = 1\n\tQuestionLinkTypeID  = 2\n)\n\ntype QuestionLink struct {\n\tLinkType   int\n\tQuestionID string\n\tAnswerID   string\n}\n\nfunc GetQuestionLink(content string) []QuestionLink {\n\tuniqueIDs := make(map[string]struct{})\n\tvar questionLinks []QuestionLink\n\n\t// use two pointer to find the link\n\tleft, right := 0, 0\n\tfor right < len(content) {\n\t\t// find \"/questions/\" or \"#\"\n\t\tswitch {\n\t\tcase right+11 < len(content) && content[right:right+11] == \"/questions/\":\n\t\t\tleft = right\n\t\t\tright += 11\n\t\t\tprocessURL(content, &left, &right, uniqueIDs, &questionLinks)\n\t\tcase content[right] == '#':\n\t\t\tleft = right + 1\n\t\t\tright = left\n\t\t\tprocessID(content, &left, &right, uniqueIDs, &questionLinks)\n\t\tdefault:\n\t\t\tright++\n\t\t}\n\t}\n\n\treturn questionLinks\n}\n\nfunc processURL(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {\n\tfor *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {\n\t\t*right++\n\t}\n\tquestionID := content[*left+len(\"/questions/\") : *right]\n\n\tanswerID := \"\"\n\tif *right < len(content) && content[*right] == '/' {\n\t\t*left = *right + 1\n\t\t*right = *left\n\t\tfor *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {\n\t\t\t*right++\n\t\t}\n\t\tanswerID = content[*left:*right]\n\t}\n\n\taddUniqueID(questionID, answerID, QuestionLinkTypeURL, uniqueIDs, questionLinks)\n}\n\nfunc processID(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {\n\tfor *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {\n\t\t*right++\n\t}\n\tid := content[*left:*right]\n\taddUniqueID(id, \"\", QuestionLinkTypeID, uniqueIDs, questionLinks)\n}\n\nfunc isDigit(c byte) bool {\n\treturn c >= '0' && c <= '9'\n}\n\nfunc isLetter(c byte) bool {\n\treturn (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')\n}\n\nfunc addUniqueID(questionID, answerID string, linkType int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {\n\tisAdd := false\n\tif answerID != \"\" {\n\t\tobjectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(answerID))\n\t\tif err != nil {\n\t\t\tanswerID = \"\"\n\t\t} else if objectType == constant.AnswerObjectType {\n\t\t\tif _, ok := uniqueIDs[answerID]; !ok {\n\t\t\t\tuniqueIDs[answerID] = struct{}{}\n\t\t\t\tisAdd = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(questionID)); err == nil {\n\t\tif _, ok := uniqueIDs[questionID]; !ok {\n\t\t\tuniqueIDs[questionID] = struct{}{}\n\t\t\tisAdd = true\n\t\t\tif objectType == constant.AnswerObjectType {\n\t\t\t\tanswerID = questionID\n\t\t\t\tquestionID = \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\tif isAdd {\n\t\t*questionLinks = append(*questionLinks, QuestionLink{\n\t\t\tLinkType:   linkType,\n\t\t\tQuestionID: questionID,\n\t\t\tAnswerID:   answerID,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/checker/question_link_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetQuestionLink(t *testing.T) {\n\t// Step 1: Test empty content\n\tt.Run(\"Empty content\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"\")\n\t\tassert.Empty(t, links)\n\t})\n\n\t// Step 2: Test content without link or ID\n\tt.Run(\"Content without link or ID\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"This is a random text\")\n\t\tassert.Empty(t, links)\n\t})\n\n\t// Step 3: Test content with valid question link\n\tt.Run(\"Valid question link\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"Check this question: https://example.com/questions/10010000000000060\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 4: Test content with valid question and answer link\n\tt.Run(\"Valid question and answer link\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"Check this answer: https://example.com/questions/10010000000000060/10020000000000060?from=copy\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"10020000000000060\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 5: Test content with #questionID\n\tt.Run(\"Content with #questionID\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"This is question #10010000000000060\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeID,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 6: Test content with #answerID\n\tt.Run(\"Content with #answerID\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"This is answer #10020000000000060\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeID,\n\t\t\t\tQuestionID: \"\",\n\t\t\t\tAnswerID:   \"10020000000000060\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 7: Test invalid question ID\n\tt.Run(\"Invalid question ID\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"https://example.com/questions/invalid\")\n\t\tassert.Empty(t, links)\n\t})\n\n\t// Step 8: Test invalid answer ID\n\tt.Run(\"Invalid answer ID\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"https://example.com/questions/10010000000000060/invalid\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 9: Test content with multiple links and IDs\n\tt.Run(\"Multiple links and IDs\", func(t *testing.T) {\n\t\tcontent := \"Question #10010000000000060 and https://example.com/questions/10010000000000060/10020000000000061 and https://example.com/questions/10010000000000065/10020000000000066 and another #10020000000000066\"\n\t\tlinks := checker.GetQuestionLink(content)\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeID,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"10020000000000061\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000065\",\n\t\t\t\tAnswerID:   \"10020000000000066\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 11: Test URL with www prefix\n\tt.Run(\"URL with www prefix\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"Check this question: https://www.example.com/questions/10010000000000060\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 12: Test URL without protocol\n\tt.Run(\"URL without protocol\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"Check this question: example.com/questions/10010000000000060\")\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000060\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n\n\t// Step 14: Test error id\n\tt.Run(\"Error id\", func(t *testing.T) {\n\t\tlinks := checker.GetQuestionLink(\"https://example.com/questions/10110000000000060\")\n\t\tassert.Empty(t, links)\n\t})\n\n\t// step 15: SEO options\n\tt.Run(\"SEO options\", func(t *testing.T) {\n\t\tcontent := `\n\t\tURL1: http://localhost:3000/questions/D1I2\n\t\tURL2: http://localhost:3000/questions/D1I2/hello\n\t\tURL3: http://localhost:3000/questions/10010000000000068\n\t\tURL4: http://localhost:3000/questions/10010000000000068/hello\n\t\tERROR URL: http://localhost:3000/questions/AAAA/BBBB\n\t\t`\n\t\tlinks := checker.GetQuestionLink(content)\n\t\tassert.Equal(t, []checker.QuestionLink{\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"D1I2\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tLinkType:   checker.QuestionLinkTypeURL,\n\t\t\t\tQuestionID: \"10010000000000068\",\n\t\t\t\tAnswerID:   \"\",\n\t\t\t},\n\t\t}, links)\n\t})\n}\n"
  },
  {
    "path": "pkg/checker/reserved_username.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/apache/answer/configs\"\n\t\"github.com/apache/answer/internal/base/path\"\n\t\"github.com/apache/answer/pkg/dir\"\n)\n\nvar (\n\treservedUsernameMapping = make(map[string]bool)\n\treservedUsernameInit    sync.Once\n)\n\nfunc initReservedUsername() {\n\treservedUsernamesJsonFilePath := filepath.Join(path.ConfigFileDir, path.DefaultReservedUsernamesConfigFileName)\n\tif dir.CheckFileExist(reservedUsernamesJsonFilePath) {\n\t\t// if reserved username file exists, read it and replace configuration\n\t\treservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath)\n\t\tif err == nil {\n\t\t\tconfigs.ReservedUsernames = reservedUsernamesJsonFile\n\t\t}\n\t}\n\tvar usernames []string\n\t_ = json.Unmarshal(configs.ReservedUsernames, &usernames)\n\tfor _, username := range usernames {\n\t\treservedUsernameMapping[username] = true\n\t}\n}\n\n// IsReservedUsername checks whether the username is reserved\nfunc IsReservedUsername(username string) bool {\n\treservedUsernameInit.Do(initReservedUsername)\n\treturn reservedUsernameMapping[username]\n}\n"
  },
  {
    "path": "pkg/checker/url.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc IsURL(str string) bool {\n\ts := strings.ToLower(str)\n\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\n\tu, err := url.Parse(s)\n\tif err != nil || u.Scheme == \"\" {\n\t\treturn false\n\t}\n\n\tif u.Host == \"\" && u.Fragment == \"\" && u.Opaque == \"\" {\n\t\treturn false\n\t}\n\treturn u.Scheme == \"http\" || u.Scheme == \"https\"\n}\n"
  },
  {
    "path": "pkg/checker/username.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\nimport \"regexp\"\n\nvar (\n\tusernameReg = regexp.MustCompile(`^[\\w.\\- ]{2,30}$`)\n)\n\nfunc IsInvalidUsername(username string) bool {\n\treturn !usernameReg.MatchString(username)\n}\n"
  },
  {
    "path": "pkg/checker/zero_string.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage checker\n\n// IsNotZeroString check s is not empty string and is not \"0\"\nfunc IsNotZeroString(s string) bool {\n\treturn len(s) > 0 && s != \"0\"\n}\n\n// FilterEmptyString filter empty string from string slice\nfunc FilterEmptyString(strs []string) []string {\n\tvar result []string\n\tfor _, str := range strs {\n\t\tif IsNotZeroString(str) {\n\t\t\tresult = append(result, str)\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "pkg/converter/array.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage converter\n\nfunc ArrayNotInArray(original []string, search []string) []string {\n\tvar result []string\n\toriginalMap := make(map[string]bool)\n\tfor _, v := range original {\n\t\toriginalMap[v] = true\n\t}\n\tfor _, v := range search {\n\t\tif _, ok := originalMap[v]; !ok {\n\t\t\tresult = append(result, v)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc UniqueArray[T comparable](input []T) []T {\n\tresult := make([]T, 0, len(input))\n\tseen := make(map[T]bool, len(input))\n\tfor _, element := range input {\n\t\tif !seen[element] {\n\t\t\tresult = append(result, element)\n\t\t\tseen[element] = true\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "pkg/converter/markdown.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage converter\n\nimport (\n\t\"bytes\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/asaskevich/govalidator\"\n\t\"github.com/microcosm-cc/bluemonday\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"github.com/yuin/goldmark\"\n\t\"github.com/yuin/goldmark/ast\"\n\t\"github.com/yuin/goldmark/extension\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/renderer\"\n\tgoldmarkHTML \"github.com/yuin/goldmark/renderer/html\"\n\t\"github.com/yuin/goldmark/util\"\n)\n\n// Markdown2HTML convert markdown to html\nfunc Markdown2HTML(source string) string {\n\tmdConverter := goldmark.New(\n\t\tgoldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM, extension.Footnote),\n\t\tgoldmark.WithParserOptions(\n\t\t\tparser.WithAutoHeadingID(),\n\t\t),\n\t\tgoldmark.WithRendererOptions(\n\t\t\tgoldmarkHTML.WithHardWraps(),\n\t\t),\n\t)\n\tvar buf bytes.Buffer\n\tif err := mdConverter.Convert([]byte(source), &buf); err != nil {\n\t\tlog.Error(err)\n\t\treturn source\n\t}\n\thtml := buf.String()\n\tfilter := bluemonday.UGCPolicy()\n\tfilter.AllowStyling()\n\tfilter.RequireNoFollowOnLinks(false)\n\tfilter.RequireParseableURLs(false)\n\tfilter.RequireNoFollowOnFullyQualifiedLinks(false)\n\tfilter.AllowElements(\"kbd\")\n\tfilter.AllowAttrs(\"title\").Matching(regexp.MustCompile(`^[\\p{L}\\p{N}\\s\\-_',\\[\\]!\\./\\\\\\(\\)]*$|^@embed?$`)).Globally()\n\tfilter.AllowAttrs(\"start\").OnElements(\"ol\")\n\thtml = strings.TrimSpace(filter.Sanitize(html))\n\treturn html\n}\n\n// Markdown2BasicHTML convert markdown to html, Only basic syntax can be used\nfunc Markdown2BasicHTML(source string) string {\n\tcontent := Markdown2HTML(source)\n\tfilter := bluemonday.NewPolicy()\n\tfilter.AllowElements(\"p\", \"b\", \"br\", \"strong\", \"em\")\n\tfilter.AllowAttrs(\"src\").OnElements(\"img\")\n\tfilter.AddSpaceWhenStrippingTag(true)\n\tcontent = filter.Sanitize(content)\n\treturn content\n}\n\ntype DangerousHTMLFilterExtension struct {\n}\n\nfunc (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {\n\tm.Renderer().AddOptions(renderer.WithNodeRenderers(\n\t\tutil.Prioritized(&DangerousHTMLRenderer{\n\t\t\tConfig: goldmarkHTML.NewConfig(),\n\t\t\tFilter: bluemonday.UGCPolicy(),\n\t\t}, 1),\n\t))\n}\n\ntype DangerousHTMLRenderer struct {\n\tgoldmarkHTML.Config\n\tFilter *bluemonday.Policy\n}\n\n// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.\nfunc (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {\n\treg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)\n\treg.Register(ast.KindRawHTML, r.renderRawHTML)\n\treg.Register(ast.KindLink, r.renderLink)\n\treg.Register(ast.KindAutoLink, r.renderAutoLink)\n}\n\nfunc (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {\n\tif !entering {\n\t\treturn ast.WalkSkipChildren, nil\n\t}\n\tn := node.(*ast.RawHTML)\n\tl := n.Segments.Len()\n\tfor i := range l {\n\t\tsegment := n.Segments.At(i)\n\t\tif string(source[segment.Start:segment.Stop]) == \"<kbd>\" || string(source[segment.Start:segment.Stop]) == \"</kbd>\" {\n\t\t\t_, _ = w.Write(segment.Value(source))\n\t\t} else {\n\t\t\t_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))\n\t\t}\n\t}\n\treturn ast.WalkSkipChildren, nil\n}\n\nfunc (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {\n\tn := node.(*ast.HTMLBlock)\n\tif entering {\n\t\tl := n.Lines().Len()\n\t\tfor i := range l {\n\t\t\tline := n.Lines().At(i)\n\t\t\tr.Writer.SecureWrite(w, line.Value(source))\n\t\t}\n\t} else if n.HasClosure() {\n\t\tclosure := n.ClosureLine\n\t\tr.Writer.SecureWrite(w, closure.Value(source))\n\t}\n\treturn ast.WalkContinue, nil\n}\n\nfunc (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {\n\tn := node.(*ast.Link)\n\tif entering && r.renderLinkIsUrl(string(n.Destination)) {\n\t\t_, _ = w.WriteString(\"<a href=\\\"\")\n\t\t// _, _ = w.WriteString(\"<a test=\\\"1\\\" rel=\\\"nofollow\\\" href=\\\"\")\n\t\tif r.Unsafe || !goldmarkHTML.IsDangerousURL(n.Destination) {\n\t\t\t_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))\n\t\t}\n\t\t_ = w.WriteByte('\"')\n\t\tif n.Title != nil {\n\t\t\t_, _ = w.WriteString(` title=\"`)\n\t\t\tr.Writer.Write(w, n.Title)\n\t\t\t_ = w.WriteByte('\"')\n\t\t}\n\t\tif n.Attributes() != nil {\n\t\t\tgoldmarkHTML.RenderAttributes(w, n, goldmarkHTML.LinkAttributeFilter)\n\t\t}\n\t\t_ = w.WriteByte('>')\n\t} else {\n\t\t_, _ = w.WriteString(\"</a>\")\n\t}\n\treturn ast.WalkContinue, nil\n}\n\nfunc (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {\n\tn := node.(*ast.AutoLink)\n\n\tif !entering || !r.renderLinkIsUrl(string(n.URL(source))) {\n\t\treturn ast.WalkContinue, nil\n\t}\n\t_, _ = w.WriteString(`<a href=\"`)\n\turl := n.URL(source)\n\tlabel := n.Label(source)\n\tif n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte(\"mailto:\")) {\n\t\t_, _ = w.WriteString(\"mailto:\")\n\t}\n\t_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))\n\tif n.Attributes() != nil {\n\t\t_ = w.WriteByte('\"')\n\t\tgoldmarkHTML.RenderAttributes(w, n, goldmarkHTML.LinkAttributeFilter)\n\t\t_ = w.WriteByte('>')\n\t} else {\n\t\t_, _ = w.WriteString(`\">`)\n\t}\n\t_, _ = w.Write(util.EscapeHTML(label))\n\t_, _ = w.WriteString(`</a>`)\n\treturn ast.WalkContinue, nil\n}\n\nfunc (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyURL string) bool {\n\tisURL := govalidator.IsURL(verifyURL)\n\tisPath, _ := regexp.MatchString(`^/`, verifyURL)\n\treturn isURL || isPath\n}\n"
  },
  {
    "path": "pkg/converter/str.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage converter\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/segmentfault/pacman/log\"\n)\n\nfunc StringToInt64(str string) int64 {\n\tnum, err := strconv.ParseInt(str, 10, 64)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn num\n}\n\nfunc StringToInt(str string) int {\n\tnum, err := strconv.Atoi(str)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn num\n}\n\nfunc IntToString(data int64) string {\n\treturn fmt.Sprintf(\"%d\", data)\n}\n\n// InterfaceToString converts data to string\n// It will be used in template render\nfunc InterfaceToString(data any) string {\n\tswitch t := data.(type) {\n\tcase int:\n\t\ti := data.(int)\n\t\treturn strconv.Itoa(i)\n\tcase int8:\n\t\ti := data.(int8)\n\t\treturn strconv.Itoa(int(i))\n\tcase int16:\n\t\ti := data.(int16)\n\t\treturn strconv.Itoa(int(i))\n\tcase int32:\n\t\ti := data.(int32)\n\t\treturn string(i)\n\tcase int64:\n\t\ti := data.(int64)\n\t\treturn strconv.FormatInt(i, 10)\n\tcase string:\n\t\treturn data.(string)\n\tdefault:\n\t\tlog.Warn(\"can't convert type:\", t)\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/converter/user.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage converter\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/segmentfault/pacman/utils\"\n)\n\nfunc DeleteUserDisplay(userID string) string {\n\treturn utils.EnShortID(StringToInt64(userID), 100)\n}\n\nfunc GetMentionUsernameList(text string) []string {\n\tre := regexp.MustCompile(`\\[@([^\\]]+)\\]\\(/users/[^\\)]+\\)`)\n\tmatches := re.FindAllStringSubmatch(text, -1)\n\n\tvar usernames []string\n\tfor _, match := range matches {\n\t\tif len(match) > 1 {\n\t\t\tusernames = append(usernames, match[1])\n\t\t}\n\t}\n\treturn usernames\n}\n"
  },
  {
    "path": "pkg/day/day.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage day\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nvar placeholder = map[string]string{\n\t\"YY\":   \"06\",      // 06\tyear\n\t\"YYYY\": \"2006\",    // 2006\tfull year\n\t\"M\":    \"1\",       // 1-12\tmonth\n\t\"MM\":   \"01\",      // 01-12\tmonth\n\t\"MMM\":  \"Jan\",     // Jan-Dec month\n\t\"MMMM\": \"January\", // January-December month\n\t\"D\":    \"2\",       // 1-31\tdate\n\t\"DD\":   \"02\",      // 01-31\tdate preset 0\n\t\"H\":    \"15\",      // 00-23\thour 24\n\t\"HH\":   \"15\",      // 00-23\thour 24\n\t\"h\":    \"3\",       // 1-12\thour 12\n\t\"hh\":   \"03\",      // 01-12\thour 12\n\t\"m\":    \"4\",       // 0-59\tminute\n\t\"mm\":   \"04\",      // 00-59\tminute\n\t\"s\":    \"5\",       // 0-59\tsecond\n\t\"ss\":   \"05\",      // 00-59\tsecond\n\t\"A\":    \"PM\",      // AM / PM\n\t\"a\":    \"pm\",      // am / pm\n\t\"[at]\": \"at\",      // at string\n}\n\nfunc Format(unix int64, format, tz string) (formatted string) {\n\t/*l := len(placeholders) - 1\n\tfor i := l; i >= 0; i-- {\n\t\tformat = strings.ReplaceAll(format, placeholders[i].old, placeholders[i].new)\n\t}*/\n\tvar toFormat strings.Builder\n\tfrom := []rune(format)\n\tfor len(from) > 0 {\n\t\tto, suffix := nextStdChunk(from)\n\t\ttoFormat.WriteString(string(to))\n\t\tfrom = suffix\n\t}\n\n\t_, _ = time.LoadLocation(tz)\n\tformatted = time.Unix(unix, 0).Format(toFormat.String())\n\treturn\n}\n\nfunc nextStdChunk(from []rune) (to, suffix []rune) {\n\tif len(from) == 0 {\n\t\tto = []rune{}\n\t\tsuffix = []rune{}\n\t\treturn\n\t}\n\n\ts := string(from[0])\n\told := \"\"\n\n\tswitch s {\n\tcase \"Y\":\n\t\tif len(from) >= 4 && string(from[:4]) == \"YYYY\" {\n\t\t\told = \"YYYY\"\n\t\t} else if len(from) >= 2 && string(from[:2]) == \"YY\" {\n\t\t\told = \"YY\"\n\t\t}\n\tcase \"M\":\n\t\tfor i := 4; i > 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"MMMM\":\n\t\t\t\t\told = \"MMMM\"\n\t\t\t\tcase \"MMM\":\n\t\t\t\t\told = \"MMM\"\n\t\t\t\tcase \"MM\":\n\t\t\t\t\told = \"MM\"\n\t\t\t\tcase \"M\":\n\t\t\t\t\told = \"M\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"D\":\n\t\tfor i := 2; i >= 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"DD\":\n\t\t\t\t\told = \"DD\"\n\t\t\t\tcase \"D\":\n\t\t\t\t\told = \"D\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"H\":\n\t\tfor i := 2; i >= 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"HH\":\n\t\t\t\t\told = \"HH\"\n\t\t\t\tcase \"H\":\n\t\t\t\t\told = \"H\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"h\":\n\t\tfor i := 2; i >= 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"hh\":\n\t\t\t\t\told = \"hh\"\n\t\t\t\tcase \"h\":\n\t\t\t\t\told = \"h\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"m\":\n\t\tfor i := 2; i >= 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"mm\":\n\t\t\t\t\told = \"mm\"\n\t\t\t\tcase \"m\":\n\t\t\t\t\told = \"m\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"s\":\n\t\tfor i := 2; i >= 0; i-- {\n\t\t\tif len(from) >= i {\n\t\t\t\tswitch string(from[:i]) {\n\t\t\t\tcase \"ss\":\n\t\t\t\t\told = \"ss\"\n\t\t\t\tcase \"s\":\n\t\t\t\t\told = \"s\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tif old != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"A\":\n\t\told = \"A\"\n\tcase \"a\":\n\t\told = \"a\"\n\tcase \"[\":\n\t\tif len(from) >= 4 && string(from[:4]) == \"[at]\" {\n\t\t\told = \"[at]\"\n\t\t}\n\tdefault:\n\t\told = s\n\t}\n\n\ttos, ok := placeholder[old]\n\tif !ok {\n\t\tto = []rune(old)\n\t} else {\n\t\tto = []rune(tos)\n\t}\n\n\tsuffix = from[len([]rune(old)):]\n\treturn\n}\n"
  },
  {
    "path": "pkg/day/day_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage day\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFormat(t *testing.T) {\n\tsec := time.Now().Unix()\n\ttz := \"Asia/Shanghai\"\n\tactual := Format(sec, \"YYYY-MM-DD HH:mm:ss\", tz)\n\t_, _ = time.LoadLocation(tz)\n\texpected := time.Unix(sec, 0).Format(\"2006-01-02 15:04:05\")\n\tassert.Equal(t, expected, actual)\n}\n"
  },
  {
    "path": "pkg/dir/dir.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage dir\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc CreateDirIfNotExist(path string) error {\n\treturn os.MkdirAll(path, os.ModePerm)\n}\n\nfunc CheckDirExist(path string) bool {\n\tf, err := os.Stat(path)\n\treturn err == nil && f.IsDir()\n}\n\nfunc CheckFileExist(path string) bool {\n\tf, err := os.Stat(path)\n\treturn err == nil && !f.IsDir()\n}\n\nfunc DirSize(path string) (int64, error) {\n\tvar size int64\n\terr := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {\n\t\tif !info.IsDir() {\n\t\t\tsize += info.Size()\n\t\t}\n\t\treturn err\n\t})\n\treturn size, err\n}\n\nfunc FormatFileSize(fileSize int64) (size string) {\n\tswitch {\n\tcase fileSize < 1024:\n\t\t// return strconv.FormatInt(fileSize, 10) + \"B\"\n\t\treturn fmt.Sprintf(\"%.2f B\", float64(fileSize)/float64(1))\n\tcase fileSize < (1024 * 1024):\n\t\treturn fmt.Sprintf(\"%.2f KB\", float64(fileSize)/float64(1024))\n\tcase fileSize < (1024 * 1024 * 1024):\n\t\treturn fmt.Sprintf(\"%.2f MB\", float64(fileSize)/float64(1024*1024))\n\tcase fileSize < (1024 * 1024 * 1024 * 1024):\n\t\treturn fmt.Sprintf(\"%.2f GB\", float64(fileSize)/float64(1024*1024*1024))\n\tcase fileSize < (1024 * 1024 * 1024 * 1024 * 1024):\n\t\treturn fmt.Sprintf(\"%.2f TB\", float64(fileSize)/float64(1024*1024*1024*1024))\n\tdefault: // if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024)\n\t\treturn fmt.Sprintf(\"%.2f EB\", float64(fileSize)/float64(1024*1024*1024*1024*1024))\n\t}\n}\n"
  },
  {
    "path": "pkg/display/url.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage display\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/pkg/htmltext\"\n\t\"github.com/apache/answer/pkg/uid\"\n)\n\n// QuestionURL get question url\nfunc QuestionURL(permalink int, siteUrl, questionID, title string) string {\n\tu := siteUrl + \"/questions\"\n\tif permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionID {\n\t\tquestionID = uid.DeShortID(questionID)\n\t} else {\n\t\tquestionID = uid.EnShortID(questionID)\n\t}\n\tu += \"/\" + questionID\n\tif permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionIDAndTitleByShortID {\n\t\tu += \"/\" + htmltext.UrlTitle(title)\n\t}\n\treturn u\n}\n\n// AnswerURL get answer url\nfunc AnswerURL(permalink int, siteUrl, questionID, title, answerID string) string {\n\tif permalink == constant.PermalinkQuestionIDAndTitle ||\n\t\tpermalink == constant.PermalinkQuestionID {\n\t\tanswerID = uid.DeShortID(answerID)\n\t} else {\n\t\tanswerID = uid.EnShortID(answerID)\n\t}\n\treturn QuestionURL(permalink, siteUrl, questionID, title) + \"/\" + answerID\n}\n\n// CommentURL get comment url\nfunc CommentURL(permalink int, siteUrl, questionID, title, answerID, commentID string) string {\n\tif len(answerID) > 0 {\n\t\treturn AnswerURL(permalink, siteUrl, questionID, answerID, title) + \"?commentId=\" + commentID\n\t}\n\treturn QuestionURL(permalink, siteUrl, questionID, title) + \"?commentId=\" + commentID\n}\n\n// UserURL get user url\nfunc UserURL(siteUrl, username string) string {\n\treturn siteUrl + \"/users/\" + username\n}\n"
  },
  {
    "path": "pkg/encryption/md5.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage encryption\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n)\n\n// MD5 return md5 hash\nfunc MD5(data string) string {\n\th := md5.New()\n\th.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n"
  },
  {
    "path": "pkg/gravatar/gravatar.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage gravatar\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// GetAvatarURL get avatar url from gravatar by email\nfunc GetAvatarURL(baseURL, email string) string {\n\thasher := sha256.Sum256([]byte(strings.TrimSpace(email)))\n\thash := hex.EncodeToString(hasher[:])\n\treturn baseURL + hash\n}\n\n// Resize resize avatar by pixel\nfunc Resize(originalAvatarURL string, sizePixel int) (resizedAvatarURL string) {\n\tif len(originalAvatarURL) == 0 {\n\t\treturn\n\t}\n\toriginalURL, err := url.Parse(originalAvatarURL)\n\tif err != nil {\n\t\treturn originalAvatarURL\n\t}\n\tquery := originalURL.Query()\n\tquery.Set(\"s\", fmt.Sprintf(\"%d\", sizePixel))\n\toriginalURL.RawQuery = query.Encode()\n\treturn originalURL.String()\n}\n"
  },
  {
    "path": "pkg/gravatar/gravatar_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage gravatar\n\nimport (\n\t\"testing\"\n\n\t\"github.com/apache/answer/internal/base/constant\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetAvatarURL(t *testing.T) {\n\ttype args struct {\n\t\temail string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"answer@answer.com\",\n\t\t\targs: args{email: \"answer@answer.com\"},\n\t\t\twant: \"https://www.gravatar.com/avatar/7296942c1f63d97f6c124705142009867638f7b3dbcdadd0cb1bcb40e427eb8e\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, GetAvatarURL(constant.DefaultGravatarBaseURL, tt.args.email))\n\t\t})\n\t}\n}\n\nfunc TestResize(t *testing.T) {\n\ttype args struct {\n\t\toriginalAvatarURL string\n\t\tsizePixel         int\n\t}\n\ttests := []struct {\n\t\tname                 string\n\t\targs                 args\n\t\twantResizedAvatarURL string\n\t}{\n\t\t{\n\t\t\tname: \"original url\",\n\t\t\targs: args{\n\t\t\t\toriginalAvatarURL: \"https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7\",\n\t\t\t\tsizePixel:         128,\n\t\t\t},\n\t\t\twantResizedAvatarURL: \"https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128\",\n\t\t},\n\t\t{\n\t\t\tname: \"already resized url\",\n\t\t\targs: args{\n\t\t\t\toriginalAvatarURL: \"https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128\",\n\t\t\t\tsizePixel:         64,\n\t\t\t},\n\t\t\twantResizedAvatarURL: \"https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=64\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty url\",\n\t\t\targs: args{\n\t\t\t\toriginalAvatarURL: \"\",\n\t\t\t\tsizePixel:         64,\n\t\t\t},\n\t\t\twantResizedAvatarURL: \"\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equalf(t, tt.wantResizedAvatarURL, Resize(tt.args.originalAvatarURL, tt.args.sizePixel), \"Resize(%v, %v)\", tt.args.originalAvatarURL, tt.args.sizePixel)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/htmltext/htmltext.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage htmltext\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/Machiel/slugify\"\n\t\"github.com/apache/answer/pkg/checker\"\n\t\"github.com/apache/answer/pkg/converter\"\n\tstrip \"github.com/grokify/html-strip-tags-go\"\n\t\"github.com/mozillazg/go-pinyin\"\n)\n\nvar (\n\treCode         = regexp.MustCompile(`(?ism)<(pre)>.*<\\/pre>`)\n\treCodeReplace  = \"{code...}\"\n\treLink         = regexp.MustCompile(`(?ism)<a.*?[^<]>(.*)?<\\/a>`)\n\treLinkReplace  = \" [$1] \"\n\treSpace        = regexp.MustCompile(` +`)\n\treSpaceReplace = \" \"\n\n\tspaceReplacer = strings.NewReplacer(\n\t\t\"\\n\", \" \",\n\t\t\"\\r\", \" \",\n\t\t\"\\t\", \" \",\n\t)\n)\n\n// ClearText clear HTML, get the clear text\nfunc ClearText(html string) string {\n\tif html == \"\" {\n\t\treturn html\n\t}\n\n\thtml = reCode.ReplaceAllString(html, reCodeReplace)\n\thtml = reLink.ReplaceAllString(html, reLinkReplace)\n\n\ttext := spaceReplacer.Replace(strip.StripTags(html))\n\n\t// replace multiple spaces to one space\n\treturn strings.TrimSpace(reSpace.ReplaceAllString(text, reSpaceReplace))\n}\n\nfunc UrlTitle(title string) (text string) {\n\ttitle = convertChinese(title)\n\ttitle = clearEmoji(title)\n\ttitle = slugify.Slugify(title)\n\ttitle = url.QueryEscape(title)\n\ttitle = cutLongTitle(title)\n\tif len(title) == 0 {\n\t\ttitle = \"topic\"\n\t}\n\treturn title\n}\n\nfunc clearEmoji(s string) string {\n\tvar ret strings.Builder\n\trs := []rune(s)\n\tfor i := range rs {\n\t\tif len(string(rs[i])) != 4 {\n\t\t\tret.WriteString(string(rs[i]))\n\t\t}\n\t}\n\treturn ret.String()\n}\n\nfunc convertChinese(content string) string {\n\thas := checker.IsChinese(content)\n\tif !has {\n\t\treturn content\n\t}\n\treturn strings.Join(pinyin.LazyConvert(content, nil), \"-\")\n}\n\nfunc cutLongTitle(title string) string {\n\tmaxBytes := 150\n\tif len(title) <= maxBytes {\n\t\treturn title\n\t}\n\n\ttruncated := title[:maxBytes]\n\tfor len(truncated) > 0 && !utf8.ValidString(truncated) {\n\t\ttruncated = truncated[:len(truncated)-1]\n\t}\n\treturn truncated\n}\n\n// FetchExcerpt return the excerpt from the HTML string\nfunc FetchExcerpt(html, trimMarker string, limit int) (text string) {\n\treturn FetchRangedExcerpt(html, trimMarker, 0, limit)\n}\n\n// findFirstMatchedWord returns the first matched word and its index\nfunc findFirstMatchedWord(text string, words []string) (string, int) {\n\tif len(text) == 0 || len(words) == 0 {\n\t\treturn \"\", 0\n\t}\n\n\twords = converter.UniqueArray(words)\n\tfirstWord := \"\"\n\tfirstIndex := len(text)\n\n\tfor _, word := range words {\n\t\tif idx := strings.Index(text, word); idx != -1 && idx < firstIndex {\n\t\t\tfirstIndex = idx\n\t\t\tfirstWord = word\n\t\t}\n\t}\n\n\tif firstIndex != len(text) {\n\t\treturn firstWord, firstIndex\n\t}\n\n\treturn \"\", 0\n}\n\n// getRuneRange returns the valid begin and end indexes of the runeText\nfunc getRuneRange(runeText []rune, offset, limit int) (begin, end int) {\n\truneLen := len(runeText)\n\n\tlimit = min(runeLen, max(0, limit))\n\tbegin = min(runeLen, max(0, offset))\n\tend = min(runeLen, begin+limit)\n\n\treturn\n}\n\n// FetchRangedExcerpt returns a ranged excerpt from the HTML string.\n// Note: offset is a rune index, not a byte index\nfunc FetchRangedExcerpt(html, trimMarker string, offset int, limit int) (text string) {\n\tif len(html) == 0 {\n\t\ttext = html\n\t\treturn\n\t}\n\n\truneText := []rune(ClearText(html))\n\tbegin, end := getRuneRange(runeText, offset, limit)\n\ttext = string(runeText[begin:end])\n\n\tif begin > 0 {\n\t\ttext = trimMarker + text\n\t}\n\tif end < len(runeText) {\n\t\ttext += trimMarker\n\t}\n\n\treturn\n}\n\n// FetchMatchedExcerpt returns the matched excerpt according to the words\nfunc FetchMatchedExcerpt(html string, words []string, trimMarker string, trimLength int) string {\n\ttext := ClearText(html)\n\tmatchedWord, matchedIndex := findFirstMatchedWord(text, words)\n\truneIndex := utf8.RuneCountInString(text[0:matchedIndex])\n\n\ttrimLength = max(0, trimLength)\n\truneOffset := runeIndex - trimLength\n\truneLimit := trimLength + trimLength + utf8.RuneCountInString(matchedWord)\n\n\ttextRuneCount := utf8.RuneCountInString(text)\n\tif runeOffset+runeLimit > textRuneCount {\n\t\t// Reserved extra chars before the matched word\n\t\truneOffset = textRuneCount - runeLimit\n\t}\n\n\treturn FetchRangedExcerpt(html, trimMarker, runeOffset, runeLimit)\n}\n\nfunc GetPicByUrl(url string) string {\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdefer func() {\n\t\t_ = res.Body.Close()\n\t}()\n\tpix, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(pix)\n}\n"
  },
  {
    "path": "pkg/htmltext/htmltext_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage htmltext\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestClearText(t *testing.T) {\n\tvar (\n\t\texpected,\n\t\tclearedText string\n\t)\n\n\t// test code clear text\n\texpected = \"hello{code...}\"\n\tclearedText = ClearText(\"<p>hello<pre>var a = \\\"good\\\"</pre></p>\")\n\tassert.Equal(t, expected, clearedText)\n\n\t// test link clear text\n\texpected = \"hello [example.com]\"\n\tclearedText = ClearText(\"<p>hello <a href=\\\"http://example.com/\\\">example.com</a></p>\")\n\tassert.Equal(t, expected, clearedText)\n\tclearedText = ClearText(\"<p>hello<a href=\\\"https://example.com/\\\">example.com</a></p>\")\n\tassert.Equal(t, expected, clearedText)\n\n\texpected = \"hello world\"\n\tclearedText = ClearText(\"<div> hello</div>\\n<div>world</div>\")\n\tassert.Equal(t, expected, clearedText)\n}\n\nfunc TestFetchExcerpt(t *testing.T) {\n\tvar (\n\t\texpected,\n\t\ttext string\n\t)\n\n\t// test english string\n\texpected = \"hello...\"\n\ttext = FetchExcerpt(\"<p>hello world</p>\", \"...\", 5)\n\tassert.Equal(t, expected, text)\n\n\t// test mixed string\n\texpected = \"hello你好...\"\n\ttext = FetchExcerpt(\"<p>hello你好world</p>\", \"...\", 7)\n\tassert.Equal(t, expected, text)\n\n\t// test mixed string with emoticon\n\texpected = \"hello你好😂...\"\n\ttext = FetchExcerpt(\"<p>hello你好😂world</p>\", \"...\", 8)\n\tassert.Equal(t, expected, text)\n\n\texpected = \"hello你好\"\n\ttext = FetchExcerpt(\"<p>hello你好</p>\", \"...\", 8)\n\tassert.Equal(t, expected, text)\n}\n\nfunc TestUrlTitle(t *testing.T) {\n\tlist := []string{\n\t\t\"hello你好😂...\",\n\t\t\"这是一个，标题，title\",\n\t}\n\tfor _, title := range list {\n\t\tformatTitle := UrlTitle(title)\n\t\tfmt.Println(formatTitle)\n\t}\n}\n\nfunc TestFindFirstMatchedWord(t *testing.T) {\n\tvar (\n\t\texpectedWord,\n\t\tactualWord string\n\t\texpectedIndex,\n\t\tactualIndex int\n\t)\n\n\ttext := \"Hello, I have 中文 and 😂 and I am supposed to work fine.\"\n\n\t// test find nothing\n\texpectedWord, expectedIndex = \"\", 0\n\tactualWord, actualIndex = findFirstMatchedWord(text, []string{\"youcantfindme\"})\n\tassert.Equal(t, expectedWord, actualWord)\n\tassert.Equal(t, expectedIndex, actualIndex)\n\n\t// test find one word\n\texpectedWord, expectedIndex = \"文\", 17\n\tactualWord, actualIndex = findFirstMatchedWord(text, []string{\"文\"})\n\tassert.Equal(t, expectedWord, actualWord)\n\tassert.Equal(t, expectedIndex, actualIndex)\n\n\t// test find multiple matched words\n\texpectedWord, expectedIndex = \"Hello\", 0\n\tactualWord, actualIndex = findFirstMatchedWord(text, []string{\"Hello\", \"文\"})\n\tassert.Equal(t, expectedWord, actualWord)\n\tassert.Equal(t, expectedIndex, actualIndex)\n}\n\nfunc TestGetRuneRange(t *testing.T) {\n\tvar (\n\t\texpectedBegin,\n\t\texpectedEnd,\n\t\tactualBegin,\n\t\tactualEnd int\n\t)\n\n\truneText := []rune(\"Hello, I have 中文 and 😂.\")\n\truneLen := len(runeText)\n\n\t// test get range of negative offset and negative limit\n\texpectedBegin, expectedEnd = 0, 0\n\tactualBegin, actualEnd = getRuneRange(runeText, -1, -1)\n\tassert.Equal(t, expectedBegin, actualBegin)\n\tassert.Equal(t, expectedEnd, actualEnd)\n\n\t// test get range of exceeding offset and exceeding limit\n\texpectedBegin, expectedEnd = runeLen, runeLen\n\tactualBegin, actualEnd = getRuneRange(runeText, runeLen+1, runeLen+1)\n\tassert.Equal(t, expectedBegin, actualBegin)\n\tassert.Equal(t, expectedEnd, actualEnd)\n\n\t// test get range of normal offset and exceeding limit\n\texpectedBegin, expectedEnd = 3, runeLen\n\tactualBegin, actualEnd = getRuneRange(runeText, 3, runeLen)\n\tassert.Equal(t, expectedBegin, actualBegin)\n\tassert.Equal(t, expectedEnd, actualEnd)\n\n\t// test get range of normal offset and normal limit\n\texpectedBegin, expectedEnd = 3, 10\n\tactualBegin, actualEnd = getRuneRange(runeText, 3, 7)\n\tassert.Equal(t, expectedBegin, actualBegin)\n\tassert.Equal(t, expectedEnd, actualEnd)\n}\n\nfunc TestFetchRangedExcerpt(t *testing.T) {\n\tvar (\n\t\texpected,\n\t\tactual string\n\t)\n\n\t// test english string\n\texpected = \"hello...\"\n\tactual = FetchRangedExcerpt(\"<p>hello world</p>\", \"...\", 0, 5)\n\tassert.Equal(t, expected, actual)\n\n\t// test string with offset\n\texpected = \"...llo你好...\"\n\tactual = FetchRangedExcerpt(\"<p>hello你好world</p>\", \"...\", 2, 5)\n\tassert.Equal(t, expected, actual)\n\n\t// test mixed string with emoticon with offset\n\texpected = \"...你好😂...\"\n\tactual = FetchRangedExcerpt(\"<p>hello你好😂world</p>\", \"...\", 5, 3)\n\tassert.Equal(t, expected, actual)\n\n\t// test mixed string with offset and exceeding limit\n\texpected = \"...你好😂world\"\n\tactual = FetchRangedExcerpt(\"<p>hello你好😂world</p>\", \"...\", 5, 100)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestCutLongTitle(t *testing.T) {\n\t// Short title, no cutting needed\n\tshort := \"hello\"\n\tassert.Equal(t, short, cutLongTitle(short))\n\n\t// Exactly max bytes, no cutting needed\n\texact150 := strings.Repeat(\"a\", 150)\n\tassert.Len(t, cutLongTitle(exact150), 150)\n\n\t// Just over max bytes, should be cut\n\texact151 := strings.Repeat(\"a\", 151)\n\tassert.Len(t, cutLongTitle(exact151), 150)\n\n\t// Multi-byte rune at boundary gets removed properly\n\tasciiPart := strings.Repeat(\"a\", 149) // 149 bytes\n\tmultiByteChar := \"中\"                  // 3 bytes - will span bytes 149-151\n\ttitle := asciiPart + multiByteChar    // 152 bytes total\n\n\tassert.Equal(t, asciiPart, cutLongTitle(title))\n}\n\nfunc TestFetchMatchedExcerpt(t *testing.T) {\n\tvar (\n\t\texpected,\n\t\tactual string\n\t)\n\n\thtml := \"<p>Hello, I have 中文 and 😂 and I am supposed to work fine</p>\"\n\n\t// test find nothing\n\t// it should return from the begin with double trimLength text\n\texpected = \"Hello, I h...\"\n\tactual = FetchMatchedExcerpt(html, []string{\"youcantfindme\"}, \"...\", 5)\n\tassert.Equal(t, expected, actual)\n\n\t// test find the word at the end\n\t// it should return the word beginning with double trimLenth plus len(word)\n\texpected = \"... work fine\"\n\tactual = FetchMatchedExcerpt(html, []string{\"youcant\", \"fine\"}, \"...\", 3)\n\tassert.Equal(t, expected, actual)\n\n\t// test find multiple words\n\t// it should return the first matched word with trimmedText\n\texpected = \"... have 中文 and 😂...\"\n\tactual = FetchMatchedExcerpt(html, []string{\"中文\", \"😂\"}, \"...\", 6)\n\tassert.Equal(t, expected, actual)\n}\n"
  },
  {
    "path": "pkg/obj/obj.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage obj\n\nimport (\n\t\"github.com/apache/answer/internal/base/constant\"\n\t\"github.com/apache/answer/internal/base/reason\"\n\t\"github.com/apache/answer/pkg/converter\"\n\t\"github.com/segmentfault/pacman/errors\"\n)\n\n// GetObjectTypeStrByObjectID get object key by object id\nfunc GetObjectTypeStrByObjectID(objectID string) (objectTypeStr string, err error) {\n\tif err := checkObjectID(objectID); err != nil {\n\t\treturn \"\", err\n\t}\n\tobjectTypeNumber := converter.StringToInt(objectID[1:4])\n\tobjectTypeStr, ok := constant.ObjectTypeNumberMapping[objectTypeNumber]\n\tif ok {\n\t\treturn objectTypeStr, nil\n\t}\n\treturn \"\", errors.BadRequest(reason.ObjectNotFound)\n}\n\n// GetObjectTypeNumberByObjectID get object type by object id\nfunc GetObjectTypeNumberByObjectID(objectID string) (objectTypeNumber int, err error) {\n\tif err := checkObjectID(objectID); err != nil {\n\t\treturn 0, err\n\t}\n\treturn converter.StringToInt(objectID[1:4]), nil\n}\n\nfunc checkObjectID(objectID string) (err error) {\n\tif len(objectID) < 5 {\n\t\treturn errors.BadRequest(reason.ObjectNotFound)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/random/random_username.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage random\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n)\n\nfunc UsernameSuffix() string {\n\tbytes := make([]byte, 2)\n\t_, _ = rand.Read(bytes)\n\treturn hex.EncodeToString(bytes)\n}\n\nfunc Username() string {\n\tbytes := make([]byte, 6)\n\t_, _ = rand.Read(bytes)\n\treturn hex.EncodeToString(bytes)\n}\n"
  },
  {
    "path": "pkg/token/token.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage token\n\nimport \"github.com/google/uuid\"\n\n// GenerateToken generate token\nfunc GenerateToken() string {\n\tuid, _ := uuid.NewV7()\n\treturn uid.String()\n}\n"
  },
  {
    "path": "pkg/uid/id.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage uid\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/bwmarrin/snowflake\"\n)\n\n// SnowFlakeID snowflake id\ntype SnowFlakeID struct {\n\t*snowflake.Node\n}\n\nvar snowFlakeIDGenerator *SnowFlakeID\n\nfunc init() {\n\tsource := rand.NewSource(time.Now().UnixNano())\n\tr := rand.New(source)\n\tnode, err := snowflake.NewNode(int64(r.Intn(1000)) + 1)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tsnowFlakeIDGenerator = &SnowFlakeID{node}\n}\n\nfunc ID() snowflake.ID {\n\tid := snowFlakeIDGenerator.Generate()\n\treturn id\n}\n\nfunc IDStr12() string {\n\tid := snowFlakeIDGenerator.Generate()\n\treturn id.Base58()\n}\n\nfunc IDStr() string {\n\tid := snowFlakeIDGenerator.Generate()\n\treturn id.Base32()\n}\n"
  },
  {
    "path": "pkg/uid/sid.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage uid\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/segmentfault/pacman/utils\"\n)\n\nconst salt = int64(100)\n\n// NumToShortID num to string\nfunc NumToShortID(id int64) string {\n\tsid := strconv.FormatInt(id, 10)\n\tif len(sid) < 17 {\n\t\treturn \"\"\n\t}\n\tsTypeCode := sid[1:4]\n\tsid = sid[4:int32(len(sid))]\n\tid, err := strconv.ParseInt(sid, 10, 64)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\ttypeCode, err := strconv.ParseInt(sTypeCode, 10, 64)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tcode := utils.EnShortID(id, salt)\n\ttcode := utils.EnShortID(typeCode, salt)\n\treturn tcode + code\n}\n\n// ShortIDToNum string to num\nfunc ShortIDToNum(code string) int64 {\n\tif len(code) < 2 {\n\t\treturn 0\n\t}\n\tscodeType := code[0:2]\n\tcode = code[2:int32(len(code))]\n\n\tid := utils.DeShortID(code, salt)\n\tcodeType := utils.DeShortID(scodeType, salt)\n\treturn 10000000000000000 + codeType*10000000000000 + id\n}\n\nfunc EnShortID(id string) string {\n\tnum, err := strconv.ParseInt(id, 10, 64)\n\tif err != nil {\n\t\treturn id\n\t}\n\treturn NumToShortID(num)\n}\n\nfunc DeShortID(sid string) string {\n\tnum, err := strconv.ParseInt(sid, 10, 64)\n\tif err != nil {\n\t\treturn strconv.FormatInt(ShortIDToNum(sid), 10)\n\t}\n\tif num < 10000000000000000 {\n\t\treturn strconv.FormatInt(ShortIDToNum(sid), 10)\n\t}\n\treturn sid\n}\n\nfunc IsShortID(id string) bool {\n\tnum, err := strconv.ParseInt(id, 10, 64)\n\tif err != nil {\n\t\treturn true\n\t}\n\tif num < 10000000000000000 {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/writer/writer.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage writer\n\nimport (\n\t\"bufio\"\n\t\"os\"\n)\n\n// ReplaceFile remove old file and write new file\nfunc ReplaceFile(filePath, content string) error {\n\t_ = os.Remove(filePath)\n\treturn WriteFile(filePath, content)\n}\n\n// WriteFile write file to path\nfunc WriteFile(filePath, content string) error {\n\tfile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\twriter := bufio.NewWriter(file)\n\tif _, err := writer.WriteString(content); err != nil {\n\t\treturn err\n\t}\n\tif err := writer.Flush(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// MoveFile move file to new path\nfunc MoveFile(oldPath, newPath string) error {\n\treturn os.Rename(oldPath, newPath)\n}\n"
  },
  {
    "path": "plugin/agent.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype Agent interface {\n\tBase\n\tRegisterUnAuthRouter(r *gin.RouterGroup)\n\tRegisterAuthUserRouter(r *gin.RouterGroup)\n\tRegisterAuthAdminRouter(r *gin.RouterGroup)\n}\n\nvar (\n\tCallAgent,\n\tregisterAgent = MakePlugin[Agent](true)\n\tsiteURLFn func() string\n)\n\n// SiteURL The site url is the domain address of the current site. e.g. http://localhost:8080\n// When some Agent plugins want to redirect to the origin site, it can use this function to get the site url.\nfunc SiteURL() string {\n\tif siteURLFn != nil {\n\t\treturn siteURLFn()\n\t}\n\treturn \"\"\n}\n\n// RegisterGetSiteURLFunc Register a function to get the site url.\nfunc RegisterGetSiteURLFunc(fn func() string) {\n\tsiteURLFn = fn\n}\n"
  },
  {
    "path": "plugin/base.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\n// Info presents the plugin information\ntype Info struct {\n\tName        Translator\n\tSlugName    string\n\tDescription Translator\n\tAuthor      string\n\tVersion     string\n\tLink        string\n}\n\n// Base is the base plugin\ntype Base interface {\n\t// Info returns the plugin information\n\tInfo() Info\n}\n\nvar (\n\t// CallBase is a function that calls all registered base plugins\n\tCallBase,\n\tregisterBase = MakePlugin[Base](true)\n)\n"
  },
  {
    "path": "plugin/cache.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\ntype Cache interface {\n\tBase\n\n\tGetString(ctx context.Context, key string) (data string, exist bool, err error)\n\tSetString(ctx context.Context, key, value string, ttl time.Duration) (err error)\n\tGetInt64(ctx context.Context, key string) (data int64, exist bool, err error)\n\tSetInt64(ctx context.Context, key string, value int64, ttl time.Duration) (err error)\n\tIncrease(ctx context.Context, key string, value int64) (data int64, err error)\n\tDecrease(ctx context.Context, key string, value int64) (data int64, err error)\n\tDel(ctx context.Context, key string) (err error)\n\tFlush(ctx context.Context) (err error)\n}\n\nvar (\n\t// CallCache is a function that calls all registered cache\n\tCallCache,\n\tregisterCache = MakePlugin[Cache](false)\n)\n"
  },
  {
    "path": "plugin/captcha.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype Captcha interface {\n\tBase\n\t// GetConfig required. Get the captcha plugin configuration.\n\t// The configuration is used to generate the captcha for frontend. Such as the token for third-party service.\n\tGetConfig() (configJsonStr string)\n\t// Create optional. If this plugin need to create captcha via backend, implement this method.\n\t// On other hand, if this plugin create captcha via third-party service, ignore this method.\n\t// Return captcha: The captcha image base64 string, code: The real captcha code.\n\tCreate() (captcha, code string)\n\t// Verify required. Verify the user input captcha is correct or not\n\t// captcha: The captchaCode generated by Create method, if not implemented, it's empty.\n\tVerify(captchaCode, userInput string) (pass bool)\n}\n\nvar (\n\t// CallCaptcha is a function that calls all registered parsers\n\tcallCaptcha,\n\tregisterCaptcha = MakePlugin[Captcha](false)\n)\n\nfunc CallCaptcha(fn func(fn Captcha) error) error {\n\tslugName := \"\"\n\t_ = callCaptcha(func(captcha Captcha) error {\n\t\tslugName = captcha.Info().SlugName\n\t\treturn nil\n\t})\n\tif slugName == \"\" {\n\t\treturn nil\n\t}\n\treturn callCaptcha(func(captcha Captcha) error {\n\t\tif captcha.Info().SlugName == slugName {\n\t\t\treturn fn(captcha)\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc CaptchaEnabled() (enabled bool) {\n\t_ = callCaptcha(func(fn Captcha) error {\n\t\tenabled = true\n\t\treturn nil\n\t})\n\treturn\n}\n\nfunc coordinatedCaptchaPlugins(slugName string) (enabledSlugNames []string) {\n\tisCaptcha := false\n\t_ = callCaptcha(func(captcha Captcha) error {\n\t\tname := captcha.Info().SlugName\n\t\tif slugName == name {\n\t\t\tisCaptcha = true\n\t\t} else {\n\t\t\tenabledSlugNames = append(enabledSlugNames, name)\n\t\t}\n\t\treturn nil\n\t})\n\tif isCaptcha {\n\t\treturn enabledSlugNames\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/cdn.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nvar (\n\tDefaultCDNFileType = map[string]bool{\n\t\t\".ico\":   true,\n\t\t\".json\":  true,\n\t\t\".css\":   true,\n\t\t\".js\":    true,\n\t\t\".webp\":  true,\n\t\t\".woff2\": true,\n\t\t\".woff\":  true,\n\t\t\".jpg\":   true,\n\t\t\".svg\":   true,\n\t\t\".png\":   true,\n\t\t\".map\":   true,\n\t\t\".txt\":   true,\n\t}\n)\n\ntype CDN interface {\n\tBase\n\tGetStaticPrefix() string\n}\n\nvar (\n\t// CallCDN is a function that calls all registered parsers\n\tCallCDN,\n\tregisterCDN = MakePlugin[CDN](false)\n)\n\nfunc coordinatedCDNPlugins(slugName string) (enabledSlugNames []string) {\n\tisCDN := false\n\t_ = CallCDN(func(cdn CDN) error {\n\t\tname := cdn.Info().SlugName\n\t\tif slugName == name {\n\t\t\tisCDN = true\n\t\t} else {\n\t\t\tenabledSlugNames = append(enabledSlugNames, name)\n\t\t}\n\t\treturn nil\n\t})\n\tif isCDN {\n\t\treturn enabledSlugNames\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype ConfigType string\ntype InputType string\n\nconst (\n\tConfigTypeInput       ConfigType = \"input\"\n\tConfigTypeTextarea    ConfigType = \"textarea\"\n\tConfigTypeCheckbox    ConfigType = \"checkbox\"\n\tConfigTypeRadio       ConfigType = \"radio\"\n\tConfigTypeSelect      ConfigType = \"select\"\n\tConfigTypeUpload      ConfigType = \"upload\"\n\tConfigTypeTimezone    ConfigType = \"timezone\"\n\tConfigTypeSwitch      ConfigType = \"switch\"\n\tConfigTypeButton      ConfigType = \"button\"\n\tConfigTypeLegend      ConfigType = \"legend\"\n\tConfigTypeTagSelector ConfigType = \"tag_selector\"\n)\n\nconst (\n\tInputTypeText     InputType = \"text\"\n\tInputTypeColor    InputType = \"color\"\n\tInputTypeDate     InputType = \"date\"\n\tInputTypeDatetime InputType = \"datetime-local\"\n\tInputTypeEmail    InputType = \"email\"\n\tInputTypeMonth    InputType = \"month\"\n\tInputTypeNumber   InputType = \"number\"\n\tInputTypePassword InputType = \"password\"\n\tInputTypeRange    InputType = \"range\"\n\tInputTypeSearch   InputType = \"search\"\n\tInputTypeTel      InputType = \"tel\"\n\tInputTypeTime     InputType = \"time\"\n\tInputTypeUrl      InputType = \"url\"\n\tInputTypeWeek     InputType = \"week\"\n)\n\ntype ConfigField struct {\n\tName        string               `json:\"name\"`\n\tType        ConfigType           `json:\"type\"`\n\tTitle       Translator           `json:\"title\"`\n\tDescription Translator           `json:\"description\"`\n\tRequired    bool                 `json:\"required\"`\n\tValue       any                  `json:\"value\"`\n\tUIOptions   ConfigFieldUIOptions `json:\"ui_options\"`\n\tOptions     []ConfigFieldOption  `json:\"options,omitempty\"`\n}\n\ntype ConfigFieldUIOptions struct {\n\tPlaceholder    Translator      `json:\"placeholder\"`\n\tRows           string          `json:\"rows,omitempty\"`\n\tInputType      InputType       `json:\"input_type,omitempty\"`\n\tLabel          Translator      `json:\"label\"`\n\tAction         *UIOptionAction `json:\"action,omitempty\"`\n\tVariant        string          `json:\"variant,omitempty\"`\n\tText           Translator      `json:\"text\"`\n\tClassName      string          `json:\"class_name,omitempty\"`\n\tFieldClassName string          `json:\"field_class_name,omitempty\"`\n}\n\ntype ConfigFieldOption struct {\n\tLabel Translator `json:\"label\"`\n\tValue string     `json:\"value\"`\n}\n\ntype UIOptionAction struct {\n\tUrl        string            `json:\"url\"`\n\tMethod     string            `json:\"method,omitempty\"`\n\tLoading    *LoadingAction    `json:\"loading,omitempty\"`\n\tOnComplete *OnCompleteAction `json:\"on_complete,omitempty\"`\n}\n\nconst (\n\tLoadingActionStateNone     LoadingActionType = \"none\"\n\tLoadingActionStatePending  LoadingActionType = \"pending\"\n\tLoadingActionStateComplete LoadingActionType = \"completed\"\n)\n\ntype LoadingActionType string\n\ntype LoadingAction struct {\n\tText  Translator        `json:\"text\"`\n\tState LoadingActionType `json:\"state\"`\n}\n\ntype OnCompleteAction struct {\n\tToastReturnMessage bool `json:\"toast_return_message\"`\n\tRefreshFormConfig  bool `json:\"refresh_form_config\"`\n}\n\n// TagSelectorOption represents a tag option in the tag selector config value field\ntype TagSelectorOption struct {\n\tTagID       string `json:\"tag_id\"`\n\tSlugName    string `json:\"slug_name\"`\n\tDisplayName string `json:\"display_name\"`\n\tRecommend   bool   `json:\"recommend\"`\n\tReserved    bool   `json:\"reserved\"`\n}\n\ntype Config interface {\n\tBase\n\n\t// ConfigFields returns the list of config fields\n\tConfigFields() []ConfigField\n\n\t// ConfigReceiver receives the config data, it calls when the config is saved or initialized.\n\t// We recommend to unmarshal the data to a struct, and then use the struct to do something.\n\t// The config is encoded in JSON format.\n\t// It depends on the definition of ConfigFields.\n\tConfigReceiver(config []byte) error\n}\n\nvar (\n\t// CallConfig is a function that calls all registered config plugins\n\tCallConfig,\n\tregisterConfig = MakePlugin[Config](true)\n)\n"
  },
  {
    "path": "plugin/connector.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype Connector interface {\n\tBase\n\n\t// ConnectorLogoSVG presents the logo in svg format\n\tConnectorLogoSVG() string\n\n\t// ConnectorName presents the name of the connector\n\t// e.g. Facebook, Twitter, Instagram\n\tConnectorName() Translator\n\n\t// ConnectorSlugName presents the slug name of the connector\n\t// Please use lowercase and hyphen as the separator\n\t// e.g. facebook, twitter, instagram\n\tConnectorSlugName() string\n\n\t// ConnectorSender presents the sender of the connector\n\t// It handles the start endpoint of the connector\n\t// receiverURL is the whole URL of the receiver\n\tConnectorSender(ctx *GinContext, receiverURL string) (redirectURL string)\n\n\t// ConnectorReceiver presents the receiver of the connector\n\t// It handles the callback endpoint of the connector, and returns the\n\tConnectorReceiver(ctx *GinContext, receiverURL string) (userInfo ExternalLoginUserInfo, err error)\n}\n\n// ExternalLoginUserInfo external login user info\ntype ExternalLoginUserInfo struct {\n\t// required. The unique user ID provided by the third-party login\n\tExternalID string\n\t// optional. This name is used preferentially during registration\n\tDisplayName string\n\t// optional. This username is used preferentially during registration\n\tUsername string\n\t// optional. If email exist will bind the existing user\n\t// IMPORTANT: The email must have been verified. If the plugin can't guarantee the email is verified, please leave it empty.\n\tEmail string\n\t// optional. The avatar URL provided by the third-party login platform\n\tAvatar string\n\t// optional. The original user information provided by the third-party login platform\n\tMetaInfo string\n}\n\nvar (\n\t// CallConnector is a function that calls all registered connectors\n\tCallConnector,\n\tregisterConnector = MakePlugin[Connector](false)\n)\n"
  },
  {
    "path": "plugin/embed.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport \"github.com/gin-gonic/gin\"\n\ntype EmbedConfig struct {\n\tPlatform string `json:\"platform\"`\n\tEnable   bool   `json:\"enable\"`\n}\n\ntype Embed interface {\n\tBase\n\tGetEmbedConfigs(ctx *gin.Context) (embedConfigs []*EmbedConfig, err error)\n}\n\nvar (\n\t// CallEmbed is a function that calls all registered parsers\n\tCallEmbed,\n\tregisterEmbed = MakePlugin[Embed](false)\n)\n"
  },
  {
    "path": "plugin/filter.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype Filter interface {\n\tBase\n\tFilterText(text string) (err error)\n}\n\nvar (\n\t// CallFilter is a function that calls all registered parsers\n\tCallFilter,\n\tregisterFilter = MakePlugin[Filter](false)\n)\n"
  },
  {
    "path": "plugin/importer.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"context\"\n)\n\ntype QuestionImporterInfo struct {\n\tTitle     string   `json:\"title\"`\n\tContent   string   `json:\"content\"`\n\tTags      []string `json:\"tags\"`\n\tUserEmail string   `json:\"user_email\"`\n}\n\ntype Importer interface {\n\tBase\n\tRegisterImporterFunc(ctx context.Context, importer ImporterFunc)\n}\n\ntype ImporterFunc interface {\n\tAddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error)\n}\n\nvar (\n\t// CallImporter is a function that calls all registered parsers\n\tCallImporter,\n\tregisterImporter = MakePlugin[Importer](false)\n)\n\nfunc ImporterEnabled() (enabled bool) {\n\t_ = CallImporter(func(fn Importer) error {\n\t\tenabled = true\n\t\treturn nil\n\t})\n\treturn\n}\nfunc GetImporter() (ip Importer, ok bool) {\n\t_ = CallImporter(func(fn Importer) error {\n\t\tip = fn\n\t\tok = true\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "plugin/kv_storage.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand/v2\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/entity\"\n\t\"github.com/segmentfault/pacman/cache\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/builder\"\n\t\"xorm.io/xorm\"\n)\n\n// Error variables for KV storage operations\nvar (\n\t// ErrKVKeyNotFound is returned when the requested key does not exist in the KV storage\n\tErrKVKeyNotFound = fmt.Errorf(\"key not found in KV storage\")\n\t// ErrKVGroupEmpty is returned when a required group name is empty\n\tErrKVGroupEmpty = fmt.Errorf(\"group name is empty\")\n\t// ErrKVKeyEmpty is returned when a required key name is empty\n\tErrKVKeyEmpty = fmt.Errorf(\"key name is empty\")\n\t// ErrKVKeyAndGroupEmpty is returned when both key and group names are empty\n\tErrKVKeyAndGroupEmpty = fmt.Errorf(\"both key and group are empty\")\n\t// ErrKVTransactionFailed is returned when a KV storage transaction operation fails\n\tErrKVTransactionFailed = fmt.Errorf(\"KV storage transaction failed\")\n)\n\n// KVParams is the parameters for KV storage operations\ntype KVParams struct {\n\tGroup    string\n\tKey      string\n\tValue    string\n\tPage     int\n\tPageSize int\n}\n\n// KVOperator provides methods to interact with the key-value storage system for plugins\ntype KVOperator struct {\n\tdata           *Data\n\tsession        *xorm.Session\n\tpluginSlugName string\n\tcacheTTL       time.Duration\n}\n\n// KVStorageOption defines a function type that configures a KVOperator\ntype KVStorageOption func(*KVOperator)\n\n// WithCacheTTL is the option to set the cache TTL; the default value is 30 minutes.\n// If ttl is less than 0, the cache will not be used\nfunc WithCacheTTL(ttl time.Duration) KVStorageOption {\n\treturn func(kv *KVOperator) {\n\t\tkv.cacheTTL = ttl\n\t}\n}\n\n// Option is used to set the options for the KV storage\nfunc (kv *KVOperator) Option(opts ...KVStorageOption) {\n\tfor _, opt := range opts {\n\t\topt(kv)\n\t}\n}\n\nfunc (kv *KVOperator) getSession(ctx context.Context) (*xorm.Session, func()) {\n\tsession := kv.session\n\tcleanup := func() {}\n\tif session == nil {\n\t\tsession = kv.data.DB.NewSession().Context(ctx)\n\t\tcleanup = func() {\n\t\t\tif session != nil {\n\t\t\t\t_ = session.Close()\n\t\t\t}\n\t\t}\n\t}\n\treturn session, cleanup\n}\n\nfunc (kv *KVOperator) getCacheKey(params KVParams) string {\n\treturn fmt.Sprintf(\"plugin_kv_storage:%s:group:%s:key:%s\", kv.pluginSlugName, params.Group, params.Key)\n}\n\nfunc (kv *KVOperator) setCache(ctx context.Context, params KVParams) {\n\tif kv.cacheTTL < 0 {\n\t\treturn\n\t}\n\n\tttl := kv.cacheTTL\n\tif ttl > 10 {\n\t\tttl += time.Duration(float64(ttl) * 0.1 * (1 - rand.Float64()))\n\t}\n\n\tcacheKey := kv.getCacheKey(params)\n\tif err := kv.data.Cache.SetString(ctx, cacheKey, params.Value, ttl); err != nil {\n\t\tlog.Warnf(\"cache set failed: %v, key: %s\", err, cacheKey)\n\t}\n}\n\nfunc (kv *KVOperator) getCache(ctx context.Context, params KVParams) (string, bool, error) {\n\tif kv.cacheTTL < 0 {\n\t\treturn \"\", false, nil\n\t}\n\n\tcacheKey := kv.getCacheKey(params)\n\treturn kv.data.Cache.GetString(ctx, cacheKey)\n}\n\nfunc (kv *KVOperator) cleanCache(ctx context.Context, params KVParams) {\n\tif kv.cacheTTL < 0 {\n\t\treturn\n\t}\n\n\tif err := kv.data.Cache.Del(ctx, kv.getCacheKey(params)); err != nil {\n\t\tlog.Warnf(\"Failed to delete cache for key %s: %v\", params.Key, err)\n\t}\n}\n\n// Get retrieves a value from KV storage by group and key.\n// Returns the value as a string or an error if the key is not found.\nfunc (kv *KVOperator) Get(ctx context.Context, params KVParams) (string, error) {\n\tif params.Key == \"\" {\n\t\treturn \"\", ErrKVKeyEmpty\n\t}\n\n\tif value, exist, err := kv.getCache(ctx, params); err == nil && exist {\n\t\treturn value, nil\n\t}\n\n\t// query\n\tdata := entity.PluginKVStorage{}\n\tquery, cleanup := kv.getSession(ctx)\n\tdefer cleanup()\n\n\tquery.Where(builder.Eq{\n\t\t\"plugin_slug_name\": kv.pluginSlugName,\n\t\t\"`group`\":          params.Group,\n\t\t\"`key`\":            params.Key,\n\t})\n\n\thas, err := query.Get(&data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !has {\n\t\treturn \"\", ErrKVKeyNotFound\n\t}\n\n\tparams.Value = data.Value\n\tkv.setCache(ctx, params)\n\n\treturn data.Value, nil\n}\n\n// Set stores a value in KV storage with the specified group and key.\n// Updates the value if it already exists.\nfunc (kv *KVOperator) Set(ctx context.Context, params KVParams) error {\n\tif params.Key == \"\" {\n\t\treturn ErrKVKeyEmpty\n\t}\n\n\tquery, cleanup := kv.getSession(ctx)\n\tdefer cleanup()\n\n\tdata := &entity.PluginKVStorage{\n\t\tPluginSlugName: kv.pluginSlugName,\n\t\tGroup:          params.Group,\n\t\tKey:            params.Key,\n\t\tValue:          params.Value,\n\t}\n\n\tkv.cleanCache(ctx, params)\n\n\taffected, err := query.Where(builder.Eq{\n\t\t\"plugin_slug_name\": kv.pluginSlugName,\n\t\t\"`group`\":          params.Group,\n\t\t\"`key`\":            params.Key,\n\t}).Cols(\"value\").Update(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif affected == 0 {\n\t\t_, err = query.Insert(data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Del removes values from KV storage by group and/or key.\n// If both group and key are provided, only that specific entry is deleted.\n// If only group is provided, all entries in that group are deleted.\n// At least one of group or key must be provided.\nfunc (kv *KVOperator) Del(ctx context.Context, params KVParams) error {\n\tif params.Key == \"\" && params.Group == \"\" {\n\t\treturn ErrKVKeyAndGroupEmpty\n\t}\n\n\tkv.cleanCache(ctx, params)\n\n\tsession, cleanup := kv.getSession(ctx)\n\tdefer cleanup()\n\n\tsession.Where(builder.Eq{\n\t\t\"plugin_slug_name\": kv.pluginSlugName,\n\t})\n\tif params.Group != \"\" {\n\t\tsession.Where(builder.Eq{\"`group`\": params.Group})\n\t}\n\tif params.Key != \"\" {\n\t\tsession.Where(builder.Eq{\"`key`\": params.Key})\n\t}\n\n\t_, err := session.Delete(&entity.PluginKVStorage{})\n\treturn err\n}\n\n// GetByGroup retrieves all key-value pairs for a specific group with pagination support.\n// Returns a map of keys to values or an error if the group is empty or not found.\nfunc (kv *KVOperator) GetByGroup(ctx context.Context, params KVParams) (map[string]string, error) {\n\tif params.Group == \"\" {\n\t\treturn nil, ErrKVGroupEmpty\n\t}\n\n\tif params.Page < 1 {\n\t\tparams.Page = 1\n\t}\n\tif params.PageSize < 1 {\n\t\tparams.PageSize = 10\n\t}\n\n\tquery, cleanup := kv.getSession(ctx)\n\tdefer cleanup()\n\n\tvar items []entity.PluginKVStorage\n\terr := query.Where(builder.Eq{\"plugin_slug_name\": kv.pluginSlugName, \"`group`\": params.Group}).\n\t\tLimit(params.PageSize, (params.Page-1)*params.PageSize).\n\t\tOrderBy(\"id ASC\").\n\t\tFind(&items)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make(map[string]string, len(items))\n\tfor _, item := range items {\n\t\tresult[item.Key] = item.Value\n\t}\n\n\treturn result, nil\n}\n\n// Tx executes a function within a transaction context. If the KVOperator already has a session,\n// it will use that session. Otherwise, it creates a new transaction session.\n// The transaction will be committed if the function returns nil, or rolled back if it returns an error.\nfunc (kv *KVOperator) Tx(ctx context.Context, fn func(ctx context.Context, kv *KVOperator) error) error {\n\tvar (\n\t\ttxKv         = kv\n\t\tshouldCommit bool\n\t)\n\n\tif kv.session == nil {\n\t\tsession := kv.data.DB.NewSession().Context(ctx)\n\t\tif err := session.Begin(); err != nil {\n\t\t\t_ = session.Close()\n\t\t\treturn fmt.Errorf(\"%w: begin transaction failed: %v\", ErrKVTransactionFailed, err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif !shouldCommit {\n\t\t\t\tif rollbackErr := session.Rollback(); rollbackErr != nil {\n\t\t\t\t\tlog.Errorf(\"rollback failed: %v\", rollbackErr)\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ = session.Close()\n\t\t}()\n\n\t\ttxKv = &KVOperator{\n\t\t\tsession:        session,\n\t\t\tdata:           kv.data,\n\t\t\tpluginSlugName: kv.pluginSlugName,\n\t\t}\n\t\tshouldCommit = true\n\t}\n\n\tif err := fn(ctx, txKv); err != nil {\n\t\treturn fmt.Errorf(\"%w: %v\", ErrKVTransactionFailed, err)\n\t}\n\n\tif shouldCommit {\n\t\tif err := txKv.session.Commit(); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: commit failed: %v\", ErrKVTransactionFailed, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// KVStorage defines the interface for plugins that need data storage capabilities\ntype KVStorage interface {\n\tInfo() Info\n\tSetOperator(operator *KVOperator)\n}\n\nvar (\n\tCallKVStorage,\n\tregisterKVStorage = MakePlugin[KVStorage](true)\n)\n\n// NewKVOperator creates a new KV storage operator with the specified database engine, cache and plugin name.\n// It returns a KVOperator instance that can be used to interact with the plugin's storage.\nfunc NewKVOperator(db *xorm.Engine, cache cache.Cache, pluginSlugName string) *KVOperator {\n\treturn &KVOperator{\n\t\tdata:           &Data{DB: db, Cache: cache},\n\t\tpluginSlugName: pluginSlugName,\n\t\tcacheTTL:       30 * time.Minute,\n\t}\n}\n"
  },
  {
    "path": "plugin/notification.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\n// NotificationType is the type of the notification\ntype NotificationType string\n\nconst (\n\tNotificationUpdateQuestion         NotificationType = \"notification.action.update_question\"\n\tNotificationAnswerTheQuestion      NotificationType = \"notification.action.answer_the_question\"\n\tNotificationUpVotedTheQuestion     NotificationType = \"notification.action.up_voted_question\"\n\tNotificationDownVotedTheQuestion   NotificationType = \"notification.action.down_voted_question\"\n\tNotificationUpdateAnswer           NotificationType = \"notification.action.update_answer\"\n\tNotificationAcceptAnswer           NotificationType = \"notification.action.accept_answer\"\n\tNotificationUpVotedTheAnswer       NotificationType = \"notification.action.up_voted_answer\"\n\tNotificationDownVotedTheAnswer     NotificationType = \"notification.action.down_voted_answer\"\n\tNotificationCommentQuestion        NotificationType = \"notification.action.comment_question\"\n\tNotificationCommentAnswer          NotificationType = \"notification.action.comment_answer\"\n\tNotificationUpVotedTheComment      NotificationType = \"notification.action.up_voted_comment\"\n\tNotificationReplyToYou             NotificationType = \"notification.action.reply_to_you\"\n\tNotificationMentionYou             NotificationType = \"notification.action.mention_you\"\n\tNotificationYourQuestionIsClosed   NotificationType = \"notification.action.your_question_is_closed\"\n\tNotificationYourQuestionWasDeleted NotificationType = \"notification.action.your_question_was_deleted\"\n\tNotificationYourAnswerWasDeleted   NotificationType = \"notification.action.your_answer_was_deleted\"\n\tNotificationYourCommentWasDeleted  NotificationType = \"notification.action.your_comment_was_deleted\"\n\tNotificationInvitedYouToAnswer     NotificationType = \"notification.action.invited_you_to_answer\"\n\tNotificationNewQuestion            NotificationType = \"notification.action.new_question\"\n\tNotificationNewQuestionFollowedTag NotificationType = \"notification.action.new_question_followed_tag\"\n)\n\ntype Notification interface {\n\tBase\n\n\t// GetNewQuestionSubscribers returns the subscribers of the new question notification\n\tGetNewQuestionSubscribers() (userIDs []string)\n\n\t// Notify sends a notification to the user\n\tNotify(msg NotificationMessage)\n}\n\ntype NotificationMessage struct {\n\t//  the type of the notification\n\tType NotificationType `json:\"notification_type\"`\n\t// the receiver user id\n\tReceiverUserID string `json:\"receiver_user_id\"`\n\t// the receiver user using language\n\tReceiverLang string `json:\"receiver_lang\"`\n\t// the receiver user external id (optional)\n\tReceiverExternalID string `json:\"receiver_external_id\"`\n\n\t// Who triggered the notification (optional, admin or system operation will not have this field)\n\tTriggerUserID string `json:\"trigger_user_id\"`\n\t// The trigger user's display name (optional, admin or system operation will not have this field)\n\tTriggerUserDisplayName string `json:\"trigger_user_display_name\"`\n\t// The trigger user's url (optional, admin or system operation will not have this field)\n\tTriggerUserUrl string `json:\"trigger_user_url\"`\n\n\t// the question title\n\tQuestionTitle string `json:\"question_title\"`\n\t// the question url\n\tQuestionUrl string `json:\"question_url\"`\n\t// the question tags (comma separated, optional, only for new question notification)\n\tQuestionTags string `json:\"tags\"`\n\n\t// the answer url (optional, only for new answer notification)\n\tAnswerUrl string `json:\"answer_url\"`\n\t// the comment url (optional, only for new comment notification)\n\tCommentUrl string `json:\"comment_url\"`\n}\n\nvar (\n\t// CallNotification is a function that calls all registered notification plugins\n\tCallNotification,\n\tregisterNotification = MakePlugin[Notification](false)\n)\n"
  },
  {
    "path": "plugin/parser.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype Parser interface {\n\tBase\n\tParse(text string) (string, error)\n}\n\nvar (\n\t// CallParser is a function that calls all registered parsers\n\tCallParser,\n\tregisterParser = MakePlugin[Parser](false)\n)\n"
  },
  {
    "path": "plugin/plugin.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"encoding/json\"\n\t\"sync\"\n\n\t\"github.com/segmentfault/pacman/cache\"\n\t\"github.com/segmentfault/pacman/i18n\"\n\t\"xorm.io/xorm\"\n\n\t\"github.com/apache/answer/internal/base/handler\"\n\t\"github.com/apache/answer/internal/base/translator\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Data is defined here to avoid circular dependency with internal/base/data\ntype Data struct {\n\tDB    *xorm.Engine\n\tCache cache.Cache\n}\n\n// GinContext is a wrapper of gin.Context\n// We export it to make it easy to use in plugins\ntype GinContext = gin.Context\n\n// StatusManager is a manager that manages the status of plugins\n// Init Plugins:\n// json.Unmarshal([]byte(`{\"plugin1\": true, \"plugin2\": false}`), &plugin.StatusManager)\n// Dump Status:\n// json.Marshal(plugin.StatusManager)\nvar StatusManager = statusManager{\n\tstatus: make(map[string]bool),\n}\n\n// Register registers a plugin\nfunc Register(p Base) {\n\tregisterBase(p)\n\n\tif _, ok := p.(Config); ok {\n\t\tregisterConfig(p.(Config))\n\t}\n\n\tif _, ok := p.(UserConfig); ok {\n\t\tregisterUserConfig(p.(UserConfig))\n\t}\n\n\tif _, ok := p.(Connector); ok {\n\t\tregisterConnector(p.(Connector))\n\t}\n\n\tif _, ok := p.(Parser); ok {\n\t\tregisterParser(p.(Parser))\n\t}\n\n\tif _, ok := p.(Filter); ok {\n\t\tregisterFilter(p.(Filter))\n\t}\n\n\tif _, ok := p.(Storage); ok {\n\t\tregisterStorage(p.(Storage))\n\t}\n\n\tif _, ok := p.(Cache); ok {\n\t\tregisterCache(p.(Cache))\n\t}\n\n\tif _, ok := p.(UserCenter); ok {\n\t\tregisterUserCenter(p.(UserCenter))\n\t}\n\n\tif _, ok := p.(Agent); ok {\n\t\tregisterAgent(p.(Agent))\n\t}\n\n\tif _, ok := p.(Search); ok {\n\t\tregisterSearch(p.(Search))\n\t}\n\n\tif _, ok := p.(Notification); ok {\n\t\tregisterNotification(p.(Notification))\n\t}\n\n\tif _, ok := p.(Reviewer); ok {\n\t\tregisterReviewer(p.(Reviewer))\n\t}\n\n\tif _, ok := p.(Captcha); ok {\n\t\tregisterCaptcha(p.(Captcha))\n\t}\n\n\tif _, ok := p.(Embed); ok {\n\t\tregisterEmbed(p.(Embed))\n\t}\n\n\tif _, ok := p.(Render); ok {\n\t\tregisterRender(p.(Render))\n\t}\n\n\tif _, ok := p.(CDN); ok {\n\t\tregisterCDN(p.(CDN))\n\t}\n\n\tif _, ok := p.(Importer); ok {\n\t\tregisterImporter(p.(Importer))\n\t}\n\n\tif _, ok := p.(KVStorage); ok {\n\t\tregisterKVStorage(p.(KVStorage))\n\t}\n\n\tif _, ok := p.(Sidebar); ok {\n\t\tregisterSidebar(p.(Sidebar))\n\t}\n}\n\ntype Stack[T Base] struct {\n\tplugins []T\n}\n\ntype RegisterFn[T Base] func(p T)\ntype Caller[T Base] func(p T) error\ntype CallFn[T Base] func(fn Caller[T]) error\n\n// MakePlugin creates a plugin caller and register stack manager\n// The parameter super presents if the plugin can be disabled.\n// It returns a register function and a caller function\n// The register function is used to register a plugin, it will be called in the plugin's init function\n// The caller function is used to call all registered plugins\nfunc MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) {\n\tstack := Stack[T]{}\n\n\tcall := func(fn Caller[T]) error {\n\t\tfor _, p := range stack.plugins {\n\t\t\t// If the plugin is disabled, skip it\n\t\t\tif !super && !StatusManager.IsEnabled(p.Info().SlugName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := fn(p); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tregister := func(p T) {\n\t\tfor _, plugin := range stack.plugins {\n\t\t\tif plugin.Info().SlugName == p.Info().SlugName {\n\t\t\t\tpanic(\"plugin \" + p.Info().SlugName + \" is already registered\")\n\t\t\t}\n\t\t}\n\t\tstack.plugins = append(stack.plugins, p)\n\t}\n\n\treturn call, register\n}\n\ntype statusManager struct {\n\tlock   sync.Mutex\n\tstatus map[string]bool\n}\n\nfunc (m *statusManager) Enable(name string, enabled bool) {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\tif !enabled {\n\t\tm.status[name] = enabled\n\t\treturn\n\t}\n\tm.status[name] = enabled\n\n\tfor _, slugName := range coordinatedCaptchaPlugins(name) {\n\t\tm.status[slugName] = false\n\t}\n\n\tfor _, slugName := range coordinatedCDNPlugins(name) {\n\t\tm.status[slugName] = false\n\t}\n}\n\nfunc (m *statusManager) IsEnabled(name string) bool {\n\tif status, ok := m.status[name]; ok {\n\t\treturn status\n\t}\n\treturn false\n}\n\n// MarshalJSON implements the json.Marshaler interface.\nfunc (m *statusManager) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(m.status)\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface.\nfunc (m *statusManager) UnmarshalJSON(data []byte) error {\n\treturn json.Unmarshal(data, &m.status)\n}\n\n// Translate translates the key to the current language of the context\nfunc Translate(ctx *GinContext, key string) string {\n\treturn translator.Tr(handler.GetLangByCtx(ctx), key)\n}\n\n// TranslateWithData translates the key to the language with data\nfunc TranslateWithData(lang i18n.Language, key string, data any) string {\n\treturn translator.TrWithData(lang, key, data)\n}\n\n// TranslateFn presents a generator of translated string.\n// We use it to delegate the translation work outside the plugin.\ntype TranslateFn func(ctx *GinContext) string\n\n// Translator contains a function that translates the key to the current language of the context\ntype Translator struct {\n\tFn TranslateFn\n}\n\n// MakeTranslator generates a translator from the key\nfunc MakeTranslator(key string) Translator {\n\tt := func(ctx *GinContext) string {\n\t\treturn Translate(ctx, key)\n\t}\n\treturn Translator{Fn: t}\n}\n\n// Translate translates the key to the current language of the context\nfunc (t Translator) Translate(ctx *GinContext) string {\n\tif t.Fn == nil {\n\t\treturn \"\"\n\t}\n\treturn t.Fn(ctx)\n}\n"
  },
  {
    "path": "plugin/plugin_test/kv_storage_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apache/answer/plugin\"\n\t\"github.com/segmentfault/pacman/log\"\n\t_ \"modernc.org/sqlite\"\n)\n\nvar (\n\ttestPlugin *TestKVStoragePlugin\n)\n\n// Helper functions for testing\nfunc mustSet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, value string) {\n\tif err := kv.Set(ctx, plugin.KVParams{Group: group, Key: key, Value: value}); err != nil {\n\t\tt.Fatalf(\"Failed to set %s/%s: %v\", group, key, err)\n\t}\n}\n\nfunc mustGet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, expected string) {\n\tval, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get %s/%s: %v\", group, key, err)\n\t}\n\tif val != expected {\n\t\tt.Errorf(\"Expected '%s' for %s/%s, got '%s'\", expected, group, key, val)\n\t}\n}\n\nfunc mustDel(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) {\n\tif err := kv.Del(ctx, plugin.KVParams{Group: group, Key: key}); err != nil {\n\t\tt.Fatalf(\"Failed to delete %s/%s: %v\", group, key, err)\n\t}\n}\n\nfunc assertNotFound(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) {\n\tval, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key})\n\tif err != plugin.ErrKVKeyNotFound {\n\t\tt.Errorf(\"Expected ErrKVKeyNotFound for %s/%s, got: %v\", group, key, err)\n\t}\n\tif val != \"\" {\n\t\tt.Errorf(\"Expected empty value for %s/%s, got: '%s'\", group, key, val)\n\t}\n}\n\nfunc assertError(t *testing.T, err error, expected error, msg string) {\n\tif err != expected {\n\t\tt.Errorf(\"%s: expected %v, got %v\", msg, expected, err)\n\t}\n}\n\n// TestKVStoragePlugin implements KVStorage interface for testing\ntype TestKVStoragePlugin struct {\n\toperator *plugin.KVOperator\n}\n\n// Info returns plugin information\nfunc (p *TestKVStoragePlugin) Info() plugin.Info {\n\treturn plugin.Info{\n\t\tName:        plugin.MakeTranslator(\"test_kv_storage_name\"),\n\t\tSlugName:    \"test_kv_storage\",\n\t\tDescription: plugin.MakeTranslator(\"test_kv_storage_desc\"),\n\t\tAuthor:      \"Answer Team\",\n\t\tVersion:     \"1.0.0\",\n\t\tLink:        \"https://github.com/apache/answer\",\n\t}\n}\n\n// SetOperator sets KV operator\nfunc (p *TestKVStoragePlugin) SetOperator(operator *plugin.KVOperator) {\n\tp.operator = operator\n}\n\n// setupTestEnvironment sets up test environment\nfunc setupTestEnvironment() {\n\t// Initialize only once\n\tif testPlugin != nil {\n\t\treturn\n\t}\n\n\t// Create and register test plugin\n\ttestPlugin = &TestKVStoragePlugin{}\n\tplugin.Register(testPlugin)\n\n\t// Enable plugin\n\tplugin.StatusManager.Enable(\"test_kv_storage\", true)\n\n\t// Initialize plugin data, refer to plugin_common_service.go implementation\n\t_ = plugin.CallKVStorage(func(k plugin.KVStorage) error {\n\t\tk.SetOperator(plugin.NewKVOperator(\n\t\t\ttestDataSource.DB,\n\t\t\ttestDataSource.Cache,\n\t\t\tk.Info().SlugName,\n\t\t))\n\t\treturn nil\n\t})\n}\n\n// Test basic operations including CRUD and edge cases\nfunc TestBasicOperations(t *testing.T) {\n\tsetupTestEnvironment()\n\tkv := testPlugin.operator\n\tctx := context.Background()\n\n\tt.Run(\"BasicCRUD\", func(t *testing.T) {\n\t\t// Set/Get\n\t\tmustSet(t, kv, ctx, \"group1\", \"key1\", \"value1\")\n\t\tmustGet(t, kv, ctx, \"group1\", \"key1\", \"value1\")\n\n\t\t// Update\n\t\tmustSet(t, kv, ctx, \"group1\", \"key1\", \"new_value\")\n\t\tmustGet(t, kv, ctx, \"group1\", \"key1\", \"new_value\")\n\n\t\t// Delete\n\t\tmustDel(t, kv, ctx, \"group1\", \"key1\")\n\t\tassertNotFound(t, kv, ctx, \"group1\", \"key1\")\n\n\t\t// Group operation\n\t\tmustSet(t, kv, ctx, \"group1\", \"key2\", \"value2\")\n\t\tmustSet(t, kv, ctx, \"group1\", \"key3\", \"value3\")\n\t\tgroupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: \"group1\", Page: 1, PageSize: 10})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get group data: %v\", err)\n\t\t}\n\n\t\t// the groupData should only have key2 and key3 because key1 is deleted\n\t\tif len(groupData) != 2 {\n\t\t\tt.Errorf(\"Expected 2 items, got %d\", len(groupData))\n\t\t}\n\t\tif groupData[\"key2\"] != \"value2\" || groupData[\"key3\"] != \"value3\" {\n\t\t\tt.Errorf(\"Unexpected group data: %v\", groupData)\n\t\t}\n\t})\n\n\tt.Run(\"EdgeCases\", func(t *testing.T) {\n\t\t// Empty key\n\t\terr := kv.Set(ctx, plugin.KVParams{Group: \"group\", Key: \"\", Value: \"value\"})\n\t\tassertError(t, err, plugin.ErrKVKeyEmpty, \"Empty key test\")\n\n\t\t// Empty group query\n\t\t_, err = kv.GetByGroup(ctx, plugin.KVParams{Group: \"\", Page: 1, PageSize: 10})\n\t\tassertError(t, err, plugin.ErrKVGroupEmpty, \"Empty group test\")\n\n\t\t// Non-existent key\n\t\tassertNotFound(t, kv, ctx, \"non_exist_group\", \"non_exist_key\")\n\n\t\t// Cache penetration protection\n\t\tkey := fmt.Sprintf(\"non_exist_key_%d\", time.Now().UnixNano())\n\t\tassertNotFound(t, kv, ctx, \"cache_penetration\", key)\n\t})\n\n\tt.Run(\"CacheConsistency\", func(t *testing.T) {\n\t\tmustSet(t, kv, ctx, \"cache_group\", \"cache_key\", \"cache_value\")\n\t\tmustGet(t, kv, ctx, \"cache_group\", \"cache_key\", \"cache_value\")\n\n\t\t// Update and verify immediate consistency\n\t\tmustSet(t, kv, ctx, \"cache_group\", \"cache_key\", \"updated_value\")\n\t\tmustGet(t, kv, ctx, \"cache_group\", \"cache_key\", \"updated_value\")\n\t})\n}\n\n// Test transactions including rollback and nested transactions\nfunc TestTransactions(t *testing.T) {\n\tsetupTestEnvironment()\n\tkv := testPlugin.operator\n\tctx := context.Background()\n\n\tt.Run(\"SuccessfulTransaction\", func(t *testing.T) {\n\t\terr := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error {\n\t\t\tif err := txKv.Set(ctx, plugin.KVParams{Group: \"tx_group\", Key: \"tx_key1\", Value: \"tx_value1\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := txKv.Set(ctx, plugin.KVParams{Group: \"tx_group\", Key: \"tx_key2\", Value: \"tx_value2\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Successful transaction failed: %v\", err)\n\t\t}\n\n\t\tmustGet(t, kv, ctx, \"tx_group\", \"tx_key1\", \"tx_value1\")\n\t\tmustGet(t, kv, ctx, \"tx_group\", \"tx_key2\", \"tx_value2\")\n\t})\n\n\tt.Run(\"TransactionRollback\", func(t *testing.T) {\n\t\terr := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error {\n\t\t\tif err := txKv.Set(ctx, plugin.KVParams{Group: \"tx_group\", Key: \"tx_key3\", Value: \"tx_value3\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"mock error\")\n\t\t})\n\t\tif err == nil {\n\t\t\tt.Error(\"Expected transaction to fail but it succeeded\")\n\t\t}\n\n\t\tassertNotFound(t, kv, ctx, \"tx_group\", \"tx_key3\")\n\t})\n\n\tt.Run(\"NestedTransactions\", func(t *testing.T) {\n\t\terr := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error {\n\t\t\tif err := txKv.Set(ctx, plugin.KVParams{Group: \"nested\", Key: \"key1\", Value: \"value1\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn txKv.Tx(ctx, func(ctx context.Context, nestedKv *plugin.KVOperator) error {\n\t\t\t\tif err := nestedKv.Set(ctx, plugin.KVParams{Group: \"nested\", Key: \"key2\", Value: \"value2\"}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"mock nested error\")\n\t\t\t})\n\t\t})\n\t\tif err == nil {\n\t\t\tt.Error(\"Expected nested transaction to fail but it succeeded\")\n\t\t}\n\n\t\t// Verify outer transaction also rolled back\n\t\tassertNotFound(t, kv, ctx, \"nested\", \"key1\")\n\t\tassertNotFound(t, kv, ctx, \"nested\", \"key2\")\n\t})\n}\n\n// Test pagination in GetByGroup\nfunc TestPagination(t *testing.T) {\n\tsetupTestEnvironment()\n\tkv := testPlugin.operator\n\tctx := context.Background()\n\ttotalItems := 25\n\n\tfor i := range totalItems {\n\t\tmustSet(t, kv, ctx, \"pagination\", fmt.Sprintf(\"key%d\", i), fmt.Sprintf(\"value%d\", i))\n\t}\n\n\t// Test pagination\n\tpage1, err := kv.GetByGroup(ctx, plugin.KVParams{Group: \"pagination\", Page: 1, PageSize: 10})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get page 1: %v\", err)\n\t}\n\tif len(page1) != 10 {\n\t\tt.Errorf(\"Page 1: expected 10 items, got %d\", len(page1))\n\t}\n\n\tpage2, err := kv.GetByGroup(ctx, plugin.KVParams{Group: \"pagination\", Page: 2, PageSize: 10})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get page 2: %v\", err)\n\t}\n\tif len(page2) != 10 {\n\t\tt.Errorf(\"Page 2: expected 10 items, got %d\", len(page2))\n\t}\n\n\tpage3, err := kv.GetByGroup(ctx, plugin.KVParams{Group: \"pagination\", Page: 3, PageSize: 10})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get page 3: %v\", err)\n\t}\n\tif len(page3) != 5 {\n\t\tt.Errorf(\"Page 3: expected 5 items, got %d\", len(page3))\n\t}\n\n\t// Verify different keys on different pages\n\tfor i := range 10 {\n\t\tkey := fmt.Sprintf(\"key%d\", i)\n\t\tif _, ok := page1[key]; !ok {\n\t\t\tt.Errorf(\"Pagination test failed, key %s should be on page 1\", key)\n\t\t}\n\t}\n\tfor i := range 10 {\n\t\tkey := fmt.Sprintf(\"key%d\", i+10)\n\t\tif _, ok := page2[key]; !ok {\n\t\t\tt.Errorf(\"Pagination test failed, key %s should be on page 2\", key)\n\t\t}\n\t}\n}\n\n// Test concurrent operations and performance\nfunc TestConcurrency(t *testing.T) {\n\tsetupTestEnvironment()\n\tkv := testPlugin.operator\n\tctx := context.Background()\n\n\tt.Run(\"BasicConcurrency\", func(t *testing.T) {\n\t\tparallel := 10\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(parallel)\n\n\t\tfor i := range parallel {\n\t\t\tgo func(index int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\ttime.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)\n\t\t\t\tmustSet(t, kv, ctx, \"concurrent\", fmt.Sprintf(\"key%d\", index), \"value\")\n\t\t\t}(i)\n\t\t}\n\t\twg.Wait()\n\n\t\t// Verify results\n\t\twg.Add(parallel)\n\t\tfor i := range parallel {\n\t\t\tgo func(index int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\ttime.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)\n\t\t\t\tmustGet(t, kv, ctx, \"concurrent\", fmt.Sprintf(\"key%d\", index), \"value\")\n\t\t\t}(i)\n\t\t}\n\t\twg.Wait()\n\t})\n\n\tt.Run(\"StressTest\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"Skipping stress test in short mode\")\n\t\t}\n\n\t\ttotalOps := 1000\n\t\tworkerCount := 20\n\t\tprefix := \"stress_test\"\n\t\topsPerWorker := totalOps / workerCount\n\n\t\tlog.Info(\"Starting KV storage stress test...\")\n\t\tstartTime := time.Now()\n\n\t\t// Concurrent write test\n\t\tvar wg sync.WaitGroup\n\t\terrorCount := int64(0)\n\n\t\tfor w := range workerCount {\n\t\t\twg.Add(1)\n\t\t\tgo func(workerID int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tstartIdx := workerID * opsPerWorker\n\n\t\t\t\tfor i := range opsPerWorker {\n\t\t\t\t\ti := startIdx + i\n\t\t\t\t\terr := kv.Set(ctx, plugin.KVParams{\n\t\t\t\t\t\tGroup: prefix,\n\t\t\t\t\t\tKey:   fmt.Sprintf(\"key%d\", i),\n\t\t\t\t\t\tValue: fmt.Sprintf(\"value%d\", i),\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Warnf(\"Write error: %v\", err)\n\t\t\t\t\t\terrorCount++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(w)\n\t\t}\n\t\twg.Wait()\n\n\t\twriteTime := time.Since(startTime)\n\n\t\t// Verify data integrity\n\t\tgroupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: prefix, Page: 1, PageSize: totalOps})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to verify data: %v\", err)\n\t\t}\n\t\tif len(groupData) != totalOps {\n\t\t\tt.Errorf(\"Data loss: expected %d items, got %d\", totalOps, len(groupData))\n\t\t}\n\n\t\t// Concurrent read test\n\t\tstartTime = time.Now()\n\t\treadErrors := int64(0)\n\n\t\twg.Add(workerCount)\n\t\tfor range workerCount {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tfor range opsPerWorker {\n\t\t\t\t\tkeyIdx := rand.Intn(totalOps)\n\t\t\t\t\tkey := fmt.Sprintf(\"key%d\", keyIdx)\n\t\t\t\t\texpected := fmt.Sprintf(\"value%d\", keyIdx)\n\n\t\t\t\t\tval, err := kv.Get(ctx, plugin.KVParams{Group: prefix, Key: key})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treadErrors++\n\t\t\t\t\t} else if val != expected {\n\t\t\t\t\t\tt.Errorf(\"Data inconsistency: key=%s, expected=%s, got=%s\", key, expected, val)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\treadTime := time.Since(startTime)\n\n\t\tlog.Infof(\"Stress test completed:\")\n\t\tlog.Infof(\"  Write: %d ops in %v (%.1f ops/sec), %d errors\", totalOps, writeTime, float64(totalOps)/writeTime.Seconds(), errorCount)\n\t\tlog.Infof(\"  Read: %d ops in %v (%.1f ops/sec), %d errors\", totalOps, readTime, float64(totalOps)/readTime.Seconds(), readErrors)\n\t})\n}\n"
  },
  {
    "path": "plugin/plugin_test/plugin_main_test.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apache/answer/internal/base/data\"\n\t\"github.com/apache/answer/internal/migrations\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n\t\"github.com/segmentfault/pacman/cache\"\n\t\"github.com/segmentfault/pacman/log\"\n\t\"xorm.io/xorm\"\n\t\"xorm.io/xorm/schemas\"\n)\n\nvar (\n\tmysqlDBSetting = TestDBSetting{\n\t\tDriver:       string(schemas.MYSQL),\n\t\tImageName:    \"mariadb\",\n\t\tImageVersion: \"10.4.7\",\n\t\tENV:          []string{\"MYSQL_ROOT_PASSWORD=root\", \"MYSQL_DATABASE=answer\", \"MYSQL_ROOT_HOST=%\"},\n\t\tPortID:       \"3306/tcp\",\n\t\tConnection:   \"root:root@(localhost:%s)/answer?parseTime=true\", // port is not fixed, it will be got by port id\n\t}\n\tpostgresDBSetting = TestDBSetting{\n\t\tDriver:       string(schemas.POSTGRES),\n\t\tImageName:    \"postgres\",\n\t\tImageVersion: \"14\",\n\t\tENV:          []string{\"POSTGRES_USER=root\", \"POSTGRES_PASSWORD=root\", \"POSTGRES_DB=answer\", \"LISTEN_ADDRESSES='*'\"},\n\t\tPortID:       \"5432/tcp\",\n\t\tConnection:   \"host=localhost port=%s user=root password=root dbname=answer sslmode=disable\",\n\t}\n\tsqlite3DBSetting = TestDBSetting{\n\t\tDriver:     string(schemas.SQLITE),\n\t\tConnection: filepath.Join(os.TempDir(), \"answer-test-data.db\"),\n\t}\n\tdbSettingMapping = map[string]TestDBSetting{\n\t\tmysqlDBSetting.Driver:    mysqlDBSetting,\n\t\tsqlite3DBSetting.Driver:  sqlite3DBSetting,\n\t\tpostgresDBSetting.Driver: postgresDBSetting,\n\t}\n\t// after all test down will execute tearDown function to clean-up\n\ttearDown func()\n\t// testDataSource used for repo testing\n\ttestDataSource *data.Data\n\ttestCache      cache.Cache\n)\n\nfunc TestMain(t *testing.M) {\n\tdbSetting, ok := dbSettingMapping[os.Getenv(\"TEST_DB_DRIVER\")]\n\tif !ok {\n\t\t// Use sqlite3 to test.\n\t\tdbSetting = dbSettingMapping[string(schemas.SQLITE)]\n\t}\n\tif dbSetting.Driver == string(schemas.SQLITE) {\n\t\t_ = os.RemoveAll(dbSetting.Connection)\n\t}\n\n\tif err := initTestDataSource(dbSetting); err != nil {\n\t\tpanic(err)\n\t}\n\tlog.Info(\"init test database successfully\")\n\n\tret := t.Run()\n\tif tearDown != nil {\n\t\ttearDown()\n\t}\n\tos.Exit(ret)\n}\n\ntype TestDBSetting struct {\n\tDriver       string\n\tImageName    string\n\tImageVersion string\n\tENV          []string\n\tPortID       string\n\tConnection   string\n}\n\nfunc initTestDataSource(dbSetting TestDBSetting) error {\n\tconnection, imageCleanUp, err := initDatabaseImage(dbSetting)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdbSetting.Connection = connection\n\n\tdbEngine, err := initDatabase(dbSetting)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewCache, err := initCache()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewData, dbCleanUp, err := data.NewData(dbEngine, newCache)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttestDataSource = newData\n\ttestCache = newCache\n\n\ttearDown = func() {\n\t\tdbCleanUp()\n\t\tlog.Info(\"cleanup test database successfully\")\n\t\timageCleanUp()\n\t\tlog.Info(\"cleanup test database image successfully\")\n\t}\n\treturn nil\n}\n\nfunc initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) {\n\t// sqlite3 don't need to set up image\n\tif dbSetting.Driver == string(schemas.SQLITE) {\n\t\treturn dbSetting.Connection, func() {\n\t\t\tlog.Info(\"remove database\", dbSetting.Connection)\n\t\t\terr = os.Remove(dbSetting.Connection)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}, nil\n\t}\n\tpool, err := dockertest.NewPool(\"\")\n\tpool.MaxWait = time.Minute * 5\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not connect to docker: %s\", err)\n\t}\n\n\t// resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV)\n\tresource, err := pool.RunWithOptions(&dockertest.RunOptions{\n\t\tRepository: dbSetting.ImageName,\n\t\tTag:        dbSetting.ImageVersion,\n\t\tEnv:        dbSetting.ENV,\n\t}, func(config *docker.HostConfig) {\n\t\tconfig.AutoRemove = true\n\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not pull resource: %s\", err)\n\t}\n\n\tconnection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID))\n\tif err := pool.Retry(func() error {\n\t\tdb, err := sql.Open(dbSetting.Driver, connection)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn db.Ping()\n\t}); err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not connect to database: %s\", err)\n\t}\n\treturn connection, func() { _ = pool.Purge(resource) }, nil\n}\n\nfunc initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) {\n\tdataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection}\n\tdbEngine, err = data.NewDB(true, dataConf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"connection to database failed: %s\", err)\n\t}\n\tif err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{\n\t\tLanguage:      \"en_US\",\n\t\tSiteName:      \"ANSWER\",\n\t\tSiteURL:       \"http://127.0.0.1:8080/\",\n\t\tContactEmail:  \"answer@answer.com\",\n\t\tAdminName:     \"admin\",\n\t\tAdminPassword: \"admin\",\n\t\tAdminEmail:    \"answer@answer.com\",\n\t}).InitDB(); err != nil {\n\t\treturn nil, fmt.Errorf(\"migrations init database failed: %s\", err)\n\t}\n\treturn dbEngine, nil\n}\n\nfunc initCache() (newCache cache.Cache, err error) {\n\tnewCache, _, err = data.NewCache(&data.CacheConf{})\n\treturn newCache, err\n}\n"
  },
  {
    "path": "plugin/render.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport \"github.com/gin-gonic/gin\"\n\ntype RenderConfig struct {\n\tSelectTheme string `json:\"select_theme\"`\n}\n\n// select_theme\n\ntype Render interface {\n\tBase\n\tGetRenderConfig(ctx *gin.Context) (renderConfig *RenderConfig)\n}\n\nvar (\n\t// CallRender is a function that calls all registered parsers\n\tCallRender,\n\tregisterRender = MakePlugin[Render](false)\n)\n"
  },
  {
    "path": "plugin/reviewer.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype Reviewer interface {\n\tBase\n\tReview(content *ReviewContent) (result *ReviewResult)\n}\n\n// ReviewContent is a struct that contains the content of a review\ntype ReviewContent struct {\n\t// The type of the content, e.g. question, answer\n\tObjectType string\n\t// The title of the content, only available for the question\n\tTitle string\n\t// The content of the review, always available\n\tContent string\n\t// The tags of the content, only available for the question\n\tTags []string\n\t// The author of the content\n\tAuthor ReviewContentAuthor\n\t// Review Language, the site language. e.g. en_US\n\t// The plugin may reply the review result according to the language\n\tLanguage string\n\t// The user agent of the request web browser\n\tUserAgent string\n\t// The IP address of the request\n\tIP string\n}\n\ntype ReviewContentAuthor struct {\n\t// The user's reputation\n\tRank int\n\t// The amount of questions that has approved\n\tApprovedQuestionAmount int64\n\t// The amount of answers that has approved\n\tApprovedAnswerAmount int64\n\t// 1:User 2:Admin 3:Moderator\n\tRole int\n}\n\ntype ReviewStatus string\n\nconst (\n\tReviewStatusApproved       ReviewStatus = \"approved\"\n\tReviewStatusDeleteDirectly ReviewStatus = \"delete_directly\"\n\tReviewStatusNeedReview     ReviewStatus = \"need_review\"\n)\n\n// ReviewResult is a struct that contains the result of a review\ntype ReviewResult struct {\n\t// If the review is approved\n\tApproved bool\n\t// The status of the review\n\tReviewStatus ReviewStatus\n\t// The reason for the result\n\tReason string\n}\n\nvar (\n\t// CallReviewer is a function that calls all registered parsers\n\tCallReviewer,\n\tregisterReviewer = MakePlugin[Reviewer](false)\n)\n"
  },
  {
    "path": "plugin/search.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\nimport (\n\t\"context\"\n)\n\ntype SearchResult struct {\n\t// ID content ID\n\tID string\n\t// Type content type, example: \"answer\", \"question\"\n\tType string\n}\n\ntype SearchContent struct {\n\tObjectID    string              `json:\"objectID\"`\n\tTitle       string              `json:\"title\"`\n\tType        string              `json:\"type\"`\n\tContent     string              `json:\"content\"`\n\tAnswers     int64               `json:\"answers\"`\n\tStatus      SearchContentStatus `json:\"status\"`\n\tTags        []string            `json:\"tags\"`\n\tQuestionID  string              `json:\"questionID\"`\n\tUserID      string              `json:\"userID\"`\n\tViews       int64               `json:\"views\"`\n\tCreated     int64               `json:\"created\"`\n\tActive      int64               `json:\"active\"`\n\tScore       int64               `json:\"score\"`\n\tHasAccepted bool                `json:\"hasAccepted\"`\n}\n\ntype SearchBasicCond struct {\n\t// From zero-based page number\n\tPage int\n\t// Page size\n\tPageSize int\n\n\t// The keywords for search.\n\tWords []string\n\t// TagIDs is a list of tag IDs.\n\tTagIDs [][]string\n\t// The object's owner user ID.\n\tUserID string\n\t// The order of the search result.\n\tOrder SearchOrderCond\n\n\t// Weathers the question is accepted or not. Only support search question.\n\tQuestionAccepted SearchAcceptedCond\n\t// Weathers the answer is accepted or not. Only support search answer.\n\tAnswerAccepted SearchAcceptedCond\n\n\t// Only support search answer.\n\tQuestionID string\n\n\t// greater than or equal to the number of votes.\n\tVoteAmount int\n\t// greater than or equal to the number of views.\n\tViewAmount int\n\t// greater than or equal to the number of answers. Only support search question.\n\tAnswerAmount int\n}\n\ntype SearchAcceptedCond int\ntype SearchContentStatus int\ntype SearchOrderCond string\n\nconst (\n\tAcceptedCondAll SearchAcceptedCond = iota\n\tAcceptedCondTrue\n\tAcceptedCondFalse\n)\n\nconst (\n\tSearchContentStatusAvailable = 1\n\tSearchContentStatusDeleted   = 10\n)\n\nconst (\n\tSearchNewestOrder    SearchOrderCond = \"newest\"\n\tSearchActiveOrder    SearchOrderCond = \"active\"\n\tSearchScoreOrder     SearchOrderCond = \"score\"\n\tSearchRelevanceOrder SearchOrderCond = \"relevance\"\n)\n\ntype Search interface {\n\tBase\n\tDescription() SearchDesc\n\tRegisterSyncer(ctx context.Context, syncer SearchSyncer)\n\tSearchContents(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error)\n\tSearchQuestions(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error)\n\tSearchAnswers(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error)\n\tUpdateContent(ctx context.Context, content *SearchContent) (err error)\n\tDeleteContent(ctx context.Context, objectID string) (err error)\n}\n\ntype SearchDesc struct {\n\t// A svg icon it wil be display in search result page. optional\n\tIcon string `json:\"icon\"`\n\t// The link address of the search engine. optional\n\tLink string `json:\"link\"`\n}\n\ntype SearchSyncer interface {\n\tGetAnswersPage(ctx context.Context, page, pageSize int) (answerList []*SearchContent, err error)\n\tGetQuestionsPage(ctx context.Context, page, pageSize int) (questionList []*SearchContent, err error)\n}\n\nvar (\n\t// CallSearch is a function that calls all registered parsers\n\tCallSearch,\n\tregisterSearch = MakePlugin[Search](false)\n)\n"
  },
  {
    "path": "plugin/sidebar.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Package plugin\npackage plugin\n\ntype SidebarConfig struct {\n\tTags      []*TagSelectorOption `json:\"tags\"`\n\tLinksText string               `json:\"links_text\"`\n}\n\ntype Sidebar interface {\n\tBase\n\tGetSidebarConfig() (sidebarConfig *SidebarConfig, err error)\n}\n\nvar (\n\t// CallRender is a function that calls all registered parsers\n\tCallSidebar,\n\tregisterSidebar = MakePlugin[Sidebar](false)\n)\n"
  },
  {
    "path": "plugin/storage.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype UploadSource string\n\nconst (\n\tUserAvatar         UploadSource = \"user_avatar\"\n\tUserPost           UploadSource = \"user_post\"\n\tUserPostAttachment UploadSource = \"user_post_attachment\"\n\tAdminBranding      UploadSource = \"admin_branding\"\n)\n\nvar (\n\tDefaultFileTypeCheckMapping = map[UploadSource]map[string]bool{\n\t\tUserAvatar: {\n\t\t\t\".jpg\":  true,\n\t\t\t\".jpeg\": true,\n\t\t\t\".png\":  true,\n\t\t\t\".webp\": true,\n\t\t},\n\t\tUserPost: {\n\t\t\t\".jpg\":  true,\n\t\t\t\".jpeg\": true,\n\t\t\t\".png\":  true,\n\t\t\t\".gif\":  true,\n\t\t\t\".webp\": true,\n\t\t},\n\t\tAdminBranding: {\n\t\t\t\".jpg\":  true,\n\t\t\t\".jpeg\": true,\n\t\t\t\".png\":  true,\n\t\t\t\".ico\":  true,\n\t\t},\n\t}\n)\n\ntype UploadFileCondition struct {\n\t// Source is the source of the file\n\tSource UploadSource\n\t// MaxImageSize is the maximum size of the image in MB\n\tMaxImageSize int\n\t// MaxAttachmentSize is the maximum size of the attachment in MB\n\tMaxAttachmentSize int\n\t// MaxImageMegapixel is the maximum megapixel of the image\n\tMaxImageMegapixel int\n\t// AuthorizedImageExtensions is the list of authorized image extensions\n\tAuthorizedImageExtensions []string\n\t// AuthorizedAttachmentExtensions is the list of authorized attachment extensions\n\tAuthorizedAttachmentExtensions []string\n}\n\ntype UploadFileResponse struct {\n\t// FullURL is the URL that can be used to access the file\n\tFullURL string\n\t// OriginalError is the error returned by the storage plugin. It is used for debugging.\n\tOriginalError error\n\t// DisplayErrorMsg is the error message that will be displayed to the user.\n\tDisplayErrorMsg Translator\n}\n\ntype Storage interface {\n\tBase\n\n\t// UploadFile uploads a file to storage.\n\t// The file is in the Form of the ctx and the key is \"file\"\n\tUploadFile(ctx *GinContext, condition UploadFileCondition) UploadFileResponse\n}\n\nvar (\n\t// CallStorage is a function that calls all registered storage\n\tCallStorage,\n\tregisterStorage = MakePlugin[Storage](false)\n)\n"
  },
  {
    "path": "plugin/user_center.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype UserCenter interface {\n\tBase\n\t// Description returns the description of the user center, including the name, icon, url, etc.\n\tDescription() UserCenterDesc\n\t// ControlCenterItems returns the items that will be displayed in the control center\n\tControlCenterItems() []ControlCenter\n\t// LoginCallback is called when the user center login callback is called\n\tLoginCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error)\n\t// SignUpCallback is called when the user center sign up callback is called\n\tSignUpCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error)\n\t// UserInfo returns the user information\n\tUserInfo(externalID string) (userInfo *UserCenterBasicUserInfo, err error)\n\t// UserStatus returns the latest user status\n\tUserStatus(externalID string) (userStatus UserStatus)\n\t// UserList returns the user list information\n\tUserList(externalIDs []string) (userInfo []*UserCenterBasicUserInfo, err error)\n\t// UserSettings returns the user settings\n\tUserSettings(externalID string) (userSettings *SettingInfo, err error)\n\t// PersonalBranding returns the personal branding information\n\tPersonalBranding(externalID string) (branding []*PersonalBranding)\n\t// AfterLogin is called after the user logs in\n\tAfterLogin(externalID, accessToken string)\n}\n\ntype UserCenterDesc struct {\n\tName                      string     `json:\"name\"`\n\tDisplayName               Translator `json:\"display_name\"`\n\tIcon                      string     `json:\"icon\"`\n\tUrl                       string     `json:\"url\"`\n\tLoginRedirectURL          string     `json:\"login_redirect_url\"`\n\tSignUpRedirectURL         string     `json:\"sign_up_redirect_url\"`\n\tRankAgentEnabled          bool       `json:\"rank_agent_enabled\"`\n\tUserStatusAgentEnabled    bool       `json:\"user_status_agent_enabled\"`\n\tUserRoleAgentEnabled      bool       `json:\"user_role_agent_enabled\"`\n\tMustAuthEmailEnabled      bool       `json:\"must_auth_email_enabled\"`\n\tEnabledOriginalUserSystem bool       `json:\"enabled_original_user_system\"`\n}\n\ntype UserStatus int\n\nconst (\n\tUserStatusAvailable UserStatus = 1\n\tUserStatusSuspended UserStatus = 9\n\tUserStatusDeleted   UserStatus = 10\n)\n\ntype UserCenterBasicUserInfo struct {\n\tExternalID  string     `json:\"external_id\"`\n\tUsername    string     `json:\"username\"`\n\tDisplayName string     `json:\"display_name\"`\n\tEmail       string     `json:\"email\"`\n\tRank        int        `json:\"rank\"`\n\tAvatar      string     `json:\"avatar\"`\n\tMobile      string     `json:\"mobile\"`\n\tBio         string     `json:\"bio\"`\n\tStatus      UserStatus `json:\"status\"`\n}\n\ntype ControlCenter struct {\n\tName  string `json:\"name\"`\n\tLabel string `json:\"label\"`\n\tUrl   string `json:\"url\"`\n}\n\ntype SettingInfo struct {\n\tProfileSettingRedirectURL string `json:\"profile_setting_redirect_url\"`\n\tAccountSettingRedirectURL string `json:\"account_setting_redirect_url\"`\n}\n\ntype PersonalBranding struct {\n\tIcon  string `json:\"icon\"`\n\tName  string `json:\"name\"`\n\tLabel string `json:\"label\"`\n\tUrl   string `json:\"url\"`\n}\n\nvar (\n\t// CallUserCenter is a function that calls all registered parsers\n\tCallUserCenter,\n\tregisterUserCenter = MakePlugin[UserCenter](false)\n)\n\nfunc UserCenterEnabled() (enabled bool) {\n\t_ = CallUserCenter(func(fn UserCenter) error {\n\t\tenabled = true\n\t\treturn nil\n\t})\n\treturn\n}\n\nfunc RankAgentEnabled() (enabled bool) {\n\t_ = CallUserCenter(func(fn UserCenter) error {\n\t\tenabled = fn.Description().RankAgentEnabled\n\t\treturn nil\n\t})\n\treturn\n}\n\nfunc GetUserCenter() (uc UserCenter, ok bool) {\n\t_ = CallUserCenter(func(fn UserCenter) error {\n\t\tuc = fn\n\t\tok = true\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "plugin/user_config.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage plugin\n\ntype UserConfig interface {\n\tBase\n\n\t// UserConfigFields returns the list of config fields\n\tUserConfigFields() []ConfigField\n\t// UserConfigReceiver receives the config data, it calls when the config is saved or initialized.\n\t// We recommend to unmarshal the data to a struct, and then use the struct to do something.\n\t// The config is encoded in JSON format.\n\t// It depends on the definition of ConfigFields.\n\tUserConfigReceiver(userID string, config []byte) error\n}\n\nvar (\n\t// CallUserConfig is a function that calls all registered config plugins\n\tCallUserConfig,\n\tregisterUserConfig = MakePlugin[UserConfig](false)\n\tgetPluginUserConfigFn func(userID, pluginSlugName string) []byte\n)\n\n// GetPluginUserConfig returns the user config of the given user id\nfunc GetPluginUserConfig(userID, pluginSlugName string) []byte {\n\tif getPluginUserConfigFn != nil {\n\t\treturn getPluginUserConfigFn(userID, pluginSlugName)\n\t}\n\treturn nil\n}\n\n// RegisterGetPluginUserConfigFunc registers a function to get the user config of the given user id\nfunc RegisterGetPluginUserConfigFunc(fn func(userID, pluginSlugName string) []byte) {\n\tgetPluginUserConfigFn = fn\n}\n"
  },
  {
    "path": "script/build_plugin.sh",
    "content": "#!/bin/bash\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nset -e\necho \"begin build plugin\"\nplugin_file=./script/plugin_list\nif [ ! -f \"$plugin_file\" ]; then\n  echo \"plugin_list is not exist\"\n  exit 0\nfi\n\necho \"plugin_list exist\"\ncmd=\"./answer build \"\nfor repo in `cat $plugin_file`\ndo\n  echo ${repo}\n  cmd=$cmd\" --with \"${repo}\ndone\n\necho \"cmd is \"$cmd\n$cmd\nif [ ! -f \"./new_answer\" ]; then\n  echo \"new_answer is not exist build failed\"\n  exit 1\nfi\nrm answer\nmv new_answer answer\n./answer plugin"
  },
  {
    "path": "script/check-asf-header.sh",
    "content": "#!/bin/bash\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# check if docker or podman is installed\nif command -v docker >/dev/null 2>&1; then\n    CONTAINER_RUNTIME=\"docker\"\nelif command -v podman >/dev/null 2>&1; then\n    CONTAINER_RUNTIME=\"podman\"\nelse\n    echo \"Neither Docker nor Podman is installed. Please install either Docker or Podman.\"\n    exit 1\nfi\n\n$CONTAINER_RUNTIME run --rm -v \"$(pwd)\":/github/workspace ghcr.io/korandoru/hawkeye-native format\n\ngofmt -w -l .\n"
  },
  {
    "path": "script/entrypoint.sh",
    "content": "#!/bin/bash\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n/usr/bin/answer init\n/usr/bin/answer upgrade\n/usr/bin/answer run -C /data/\n"
  },
  {
    "path": "script/gen-api.sh",
    "content": "#!/bin/bash\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\ncd ../\nswag init --generalInfo ./cmd/answer/main.go\n"
  },
  {
    "path": "script/plugin_list",
    "content": "github.com/apache/answer-plugins/connector-basic@latest\ngithub.com/apache/answer-plugins/reviewer-basic@latest\ngithub.com/apache/answer-plugins/captcha-basic@latest\ngithub.com/apache/answer-plugins/quick-links@latest"
  },
  {
    "path": "ui/.browserslistrc",
    "content": "[production]\n> 20%\nnot dead\nnot op_mini all\n\n[development]\nlast 1 chrome version\nlast 1 firefox version\nlast 1 safari version\n\n[ssr]\nnode 16\n"
  },
  {
    "path": "ui/.editorconfig",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\n# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nmax_line_length = 80\n\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": "ui/.eslintignore",
    "content": "public\nconfig-overrides.js\ncommitlint.config.js\nbuild\n.eslintrc.js\nnode_modules/\nsrc/types/\nscripts/\nsrc/plugins/**\n!src/plugins/builtin\n"
  },
  {
    "path": "ui/.eslintrc.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nmodule.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es2021: true,\n  },\n  extends: [\n    'react-app/jest',\n    'plugin:react/recommended',\n    'airbnb',\n    'airbnb-typescript',\n    'plugin:import/typescript',\n    'plugin:prettier/recommended',\n  ],\n  overrides: [],\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaFeatures: {\n      jsx: true,\n    },\n    ecmaVersion: 'latest',\n    sourceType: 'module',\n    tsconfigRootDir: __dirname,\n    project: ['./tsconfig.json'],\n  },\n  plugins: ['react', '@typescript-eslint', 'prettier'],\n  rules: {\n    'prettier/prettier': 'error',\n    'no-unused-vars': 'off',\n    'no-console': 'off',\n    'import/prefer-default-export': 'off',\n    'no-param-reassign': 'off',\n    'react/react-in-jsx-scope': 'off',\n    'react/function-component-definition': 'off',\n    'react/button-has-type': 'off',\n    'react/no-unescaped-entities': 'off',\n    'react/require-default-props': 'off',\n    'arrow-body-style': 'off',\n    \"global-require\": \"off\",\n    'react/prop-types': 0,\n    'react/no-danger': 'off',\n    'jsx-a11y/no-static-element-interactions': 'off',\n    'jsx-a11y/label-has-associated-control': 'off',\n    'jsx-a11y/tabindex-no-positive': 'off',\n    'jsx-a11y/control-has-associated-label': 'off',\n    'func-names': 'off',\n    'no-alert': 'off',\n    'prefer-promise-reject-errors': 'off',\n    '@typescript-eslint/naming-convention': 'off',\n    'no-debugger': 'off',\n    'max-len': 'off',\n    'import/extensions': 'off',\n    'react-hooks/exhaustive-deps': 'off',\n    'react/jsx-props-no-spreading': 'off',\n    '@typescript-eslint/default-param-last': 'off',\n    'no-nested-ternary': 'off',\n    'class-methods-use-this': 'off',\n    'import/order': [\n      'error',\n      {\n        groups: [\n          'builtin',\n          'external',\n          ['internal', 'parent', 'sibling', 'index'],\n          'unknown',\n        ],\n        pathGroups: [\n          {\n            pattern: 'react*',\n            group: 'external',\n            position: 'before',\n          },\n          {\n            pattern: '@/**',\n            group: 'internal',\n          },\n          {\n            pattern: './**',\n            group: 'internal',\n            position: 'after',\n          },\n          {\n            pattern: '*.scss',\n            patternOptions: { matchBase: true },\n            group: 'unknown',\n            position: 'after',\n          },\n        ],\n        pathGroupsExcludedImportTypes: ['react'],\n        'newlines-between': 'always',\n      },\n    ],\n    'jsx-a11y/click-events-have-key-events': 'off',\n    'jsx-a11y/no-noninteractive-tabindex': 'off',\n  },\n};\n"
  },
  {
    "path": "ui/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n\n/build/*/*/*\n/build/*.json\n/build/*.html\n/build/*.txt\n\n# misc\n.DS_Store\n.env*.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nThumbs.db\n.idea/\n*.sublime-project\n*.sublime-workspace\n*.log\nyarn.lock\npackage-lock.json\n.eslintcache\n/.vscode/\n\n/* !/src/plugins\n/src/plugins/*\n!/src/plugins/builtin\n!/src/plugins/Demo\n/src/plugins/*/*.go\n"
  },
  {
    "path": "ui/.lintstagedrc.json",
    "content": "{\n  \"src/**/*.{ts,tsx}\": [\n    \"eslint --fix\",\n    \"prettier --write\"\n  ],\n  \"src/**/*.{scss,md}\": [\n    \"prettier --write\"\n  ]\n}"
  },
  {
    "path": "ui/.npmrc",
    "content": "strict-peer-dependencies = true\nauto-install-peers = true\n"
  },
  {
    "path": "ui/.prettierrc.json",
    "content": "{\n  \"trailingComma\": \"all\",\n  \"tabWidth\": 2,\n  \"singleQuote\": true,\n  \"jsxBracketSameLine\": true,\n  \"printWidth\": 80,\n  \"endOfLine\": \"auto\",\n  \"bracketSameLine\": true\n}\n"
  },
  {
    "path": "ui/commitlint.config.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nmodule.exports = {\n  extends: ['@commitlint/config-conventional'],\n};\n"
  },
  {
    "path": "ui/config-overrides.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst {\n  addWebpackModuleRule,\n  addWebpackAlias,\n  setWebpackOptimizationSplitChunks,\n  addWebpackPlugin,\n} = require(\"customize-cra\");\nconst webpack = require('webpack');\n\nconst path = require(\"path\");\nconst i18nPath = path.resolve(__dirname, \"../i18n\");\n\nmodule.exports = {\n  webpack: function(config, env) {\n    addWebpackAlias({\n      \"@\": path.resolve(__dirname, \"src\"),\n      \"@i18n\": i18nPath,\n      buffer: 'buffer',\n    })(config);\n\n    addWebpackModuleRule({\n      test: /\\.ya?ml$/,\n      use: \"yaml-loader\"\n    })(config);\n\n    addWebpackPlugin(\n      new webpack.ProvidePlugin({\n        Buffer: ['buffer', 'Buffer'],\n      })\n    )(config);\n\n    setWebpackOptimizationSplitChunks({\n      maxInitialRequests: 20,\n      minSize: 20 * 1024,\n      minChunks: 2,\n      cacheGroups: {\n        automaticNamePrefix: 'chunk',\n        mix1: {\n          test: (module, chunks) => {\n            return (\n              module.resource &&\n              (module.resource.includes('components') ||\n                /\\/node_modules\\/react-bootstrap\\//.test(module.resource))\n            );\n          },\n          name: 'chunk-mix1',\n          filename: 'static/js/[name].[contenthash:8].chunk.js',\n          priority: 14,\n          reuseExistingChunk: true,\n          minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,\n          chunks: 'initial',\n        },\n        mix2: {\n          name: 'chunk-mix2',\n          test: /[\\/]node_modules[\\/](i18next|lodash|marked|next-share)[\\/]/,\n          filename: 'static/js/[name].[contenthash:8].chunk.js',\n          priority: 13,\n          reuseExistingChunk: true,\n          minChunks: 1,\n          chunks: 'initial',\n        },\n        mix3: {\n          name: 'chunk-mix3',\n          test: /[\\/]node_modules[\\/](@remix-run|@restart|axios|diff)[\\/]/,\n          filename: 'static/js/[name].[contenthash:8].chunk.js',\n          priority: 12,\n          reuseExistingChunk: true,\n          minChunks: 1,\n          chunks: 'initial',\n        },\n        codemirror: {\n          name: 'codemirror',\n          test: /[\\/]node_modules[\\/](\\@codemirror)[\\/]/,\n          priority: 10,\n          reuseExistingChunk: true,\n          minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,\n          chunks: 'initial',\n          enforce: true,\n        },\n        lezer: {\n          name: 'lezer',\n          test: /[\\/]node_modules[\\/](\\@lezer)[\\/]/,\n          priority: 9,\n          reuseExistingChunk: true,\n          minChunks: process.env.NODE_ENV === 'production' ? 1 : 2,\n          chunks: 'initial',\n          enforce: true,\n        },\n        reactDom: {\n          name: 'react-dom',\n          test: /[\\/]node_modules[\\/](react-dom)[\\/]/,\n          filename: 'static/js/[name].[contenthash:8].chunk.js',\n          priority: 8,\n          reuseExistingChunk: true,\n          chunks: 'all',\n          enforce: true,\n        },\n        nodesInitial: {\n          name: 'chunk-nodesInitial',\n          filename: 'static/js/[name].[contenthash:8].chunk.js',\n          test: /[\\/]node_modules[\\/]/,\n          priority: 1,\n          minChunks: 1,\n          chunks: 'initial',\n          reuseExistingChunk: true,\n        },\n      },\n    })(config);\n\n    // add i18n dir to ModuleScopePlugin allowedPaths\n    const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === \"ModuleScopePlugin\");\n    if (moduleScopePlugin) {\n      moduleScopePlugin.allowedPaths.push(i18nPath);\n    }\n\n    return config;\n  },\n  devServer: function(configFunction) {\n    return function(proxy, allowedHost) {\n      const config = configFunction(proxy, allowedHost);\n      config.proxy = [\n        {\n          context: ['/answer', '/installation'],\n          target: process.env.REACT_APP_API_URL,\n          changeOrigin: true,\n          secure: false,\n        },\n        {\n          context: ['/custom.css'],\n          target: process.env.REACT_APP_API_URL,\n        }\n      ];\n      return config;\n    };\n  }\n};\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"name\": \"answer-static\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"homepage\": \"/\",\n  \"scripts\": {\n    \"start\": \"react-app-rewired start\",\n    \"build\": \"node ./scripts/env.js && react-app-rewired build\",\n    \"pre-install\": \"node ./scripts/importPlugins.js && pnpm install && node ./scripts/preinstall.js \",\n    \"prepare\": \"pnpm build:packages\",\n    \"build:packages\": \"pnpm -r --filter=./src/plugins/* run build\",\n    \"clean\": \"rm -rf node_modules && rm -rf src/plugins/**/node_modules\",\n    \"analyze\": \"source-map-explorer 'build/static/js/*.js'\",\n    \"setup-lint\": \"node scripts/setup-eslint.js && cd .. && husky install\",\n    \"lint\": \"eslint . --cache --fix --ext .ts,.tsx\",\n    \"prettier\": \"prettier --write \\\"src/**/*.{ts,tsx,css,scss,md}\\\"\",\n    \"lint-staged\": \"lint-staged\"\n  },\n  \"dependencies\": {\n    \"@codemirror/lang-markdown\": \"^6.2.4\",\n    \"@codemirror/language-data\": \"^6.5.0\",\n    \"@codemirror/state\": \"^6.5.0\",\n    \"@codemirror/view\": \"^6.26.1\",\n    \"axios\": \"^1.7.7\",\n    \"bootstrap\": \"^5.3.2\",\n    \"bootstrap-icons\": \"^1.10.5\",\n    \"classnames\": \"^2.3.1\",\n    \"codemirror\": \"^6.0.1\",\n    \"color\": \"^4.2.3\",\n    \"copy-to-clipboard\": \"^3.3.2\",\n    \"dayjs\": \"^1.11.5\",\n    \"diff\": \"^5.1.0\",\n    \"front-matter\": \"^4.0.2\",\n    \"i18next\": \"^21.9.0\",\n    \"js-sha256\": \"0.11.0\",\n    \"lodash\": \"^4.17.21\",\n    \"marked\": \"^4.0.19\",\n    \"next-share\": \"^0.18.1\",\n    \"qrcode\": \"^1.5.1\",\n    \"qs\": \"^6.11.0\",\n    \"react\": \"^18.2.0\",\n    \"react-bootstrap\": \"^2.10.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-helmet-async\": \"^1.3.0\",\n    \"react-i18next\": \"^11.18.3\",\n    \"react-router-dom\": \"^7.0.2\",\n    \"semver\": \"^7.3.8\",\n    \"swr\": \"^1.3.0\",\n    \"uuid\": \"13.0.0\",\n    \"zustand\": \"^5.0.2\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^17.0.3\",\n    \"@commitlint/config-conventional\": \"^17.2.0\",\n    \"@fullhuman/postcss-purgecss\": \"^4.1.3\",\n    \"@testing-library/dom\": \"^8.17.1\",\n    \"@testing-library/jest-dom\": \"^4.2.4\",\n    \"@testing-library/react\": \"^13.3.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/color\": \"^3.0.3\",\n    \"@types/dompurify\": \"^2.4.0\",\n    \"@types/jest\": \"^27.5.2\",\n    \"@types/lodash\": \"^4.14.184\",\n    \"@types/marked\": \"^4.0.6\",\n    \"@types/node\": \"^16.11.47\",\n    \"@types/qs\": \"^6.9.7\",\n    \"@types/react\": \"^18.0.17\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.11.0\",\n    \"@typescript-eslint/parser\": \"^6.11.0\",\n    \"buffer\": \"6.0.3\",\n    \"customize-cra\": \"^1.0.0\",\n    \"eslint\": \"^8.53.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-airbnb-typescript\": \"^17.1.0\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-config-standard-with-typescript\": \"^39.1.1\",\n    \"eslint-plugin-import\": \"^2.25.2\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-n\": \"^15.0.0 || ^16.0.0 \",\n    \"eslint-plugin-prettier\": \"^5.0.0\",\n    \"eslint-plugin-promise\": \"^6.0.0\",\n    \"eslint-plugin-react\": \"^7.33.2\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"husky\": \"^9.1.7\",\n    \"js-yaml\": \"^4.1.0\",\n    \"lint-staged\": \"^15.5.0\",\n    \"postcss\": \"^8.0.0\",\n    \"prettier\": \"^3.1.0\",\n    \"purgecss-webpack-plugin\": \"^4.1.3\",\n    \"react-app-rewired\": \"^2.2.1\",\n    \"react-scripts\": \"5.0.1\",\n    \"sass\": \"1.54.4\",\n    \"source-map-explorer\": \"^2.5.3\",\n    \"typescript\": \"^4.9.5\",\n    \"yaml-loader\": \"^0.8.0\"\n  },\n  \"packageManager\": \"pnpm@9.7.0\",\n  \"engines\": {\n    \"node\": \">=20\",\n    \"pnpm\": \">=9\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "ui/pnpm-workspace.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\npackages:\n  - \"src/plugins/**\"\n  - \"!src/plugins/builtin/**\"\n"
  },
  {
    "path": "ui/public/index.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n<!DOCTYPE html>\n<html data-bs-theme=\"\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n    <meta name=\"theme-color\" />\n    <meta name=\"generator\" content=\"Answer - https://github.com/apache/answer\">\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\">\n      <div id=\"spin-mask\">\n        <noscript>\n          <style>\n            #spin-mask {\n              display: none !important;\n            }\n\n            #protect-browser {\n              display: none;\n            }\n          </style>\n        </noscript>\n        <style>\n          @keyframes _doc-spin {\n            to { transform: rotate(360deg) }\n          }\n          #spin-mask {\n            position: fixed;\n            top: 0;\n            right: 0;\n            bottom: 0;\n            left: 0;\n            background-color: white;\n            z-index: 9999;\n          }\n          #spin-container {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n          }\n          #spin-container .spinner {\n            box-sizing: border-box;\n            display: inline-block;\n            width: 2rem;\n            height: 2rem;\n            vertical-align: -.125em;\n            border: .25rem solid currentColor;\n            border-right-color: transparent;\n            color: rgba(108, 117, 125, .75);\n            border-radius: 50%;\n            animation: 0.75s linear infinite _doc-spin;\n          }\n\n          #protect-browser {\n            padding: 20px;\n            text-align: center;\n          }\n        </style>\n        <div id=\"spin-container\">\n          <div class=\"spinner\"></div>\n        </div>\n        <div id=\"protect-browser\"></div>\n      </div>\n\n    </div>\n  </body>\n  <script>\n    // disable react devtools\n    if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {\n      window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};\n    }\n    /**\n     * @description: Prompt that the browser version is too low\n    */\n    const defaultList = [\n      {\n        name: 'Edge',\n        version: '100'\n      },\n      {\n        name: 'Firefox',\n        version: '100'\n      },\n      {\n        name: 'Chrome',\n        version: '90'\n      },\n      {\n        name: 'Safari',\n        version: '15'\n      },\n      {\n        name: 'IE',\n      }\n    ];\n    function getBrowserTypeAndVersion(){\n      var browser = {\n        name: '',\n        version: ''\n      };\n      var ua = navigator.userAgent.toLowerCase();\n      var s;\n      ((ua.indexOf(\"compatible\") > -1 && ua.indexOf(\"MSIE\") > -1) || (ua.indexOf('Trident') > -1 && ua.indexOf(\"rv:11.0\") > -1)) ? browser = { name: 'IE', version: '' } :\n      (s = ua.match(/edge\\/([\\d\\.]+)/)) ? browser = { name: 'Edge', version: s[1] } :\n      (s = ua.match(/firefox\\/([\\d\\.]+)/)) ? browser = { name: 'Firefox', version: s[1] } :\n      (s = ua.match(/chrome\\/([\\d\\.]+)/)) ?  browser = { name: 'Chrome', version: s[1] } :\n      (s = ua.match(/version\\/([\\d\\.]+).*safari/)) ?  browser = { name: 'Safari', version: s[1] } : browser = { name: 'unknown', version: '' };\n      // 根据关系进行判断\n      return browser;\n    }\n\n    function compareVersion(version1, version2) {\n      var v1 = version1.split('.');\n      var v2 = version2.split('.');\n      var len = Math.max(v1.length, v2.length);\n      while (v1.length < len) {\n        v1.push('0');\n      }\n      while (v2.length < len) {\n        v2.push('0');\n      }\n      for (var i = 0; i < len; i++) {\n        var num1 = parseInt(v1[i]);\n        var num2 = parseInt(v2[i]);\n        if (num1 >= num2) {\n          return 1;\n        } else if (num1 < num2) {\n          return -1;\n        }\n      }\n      return 0;\n    }\n\n    const browserInfo = getBrowserTypeAndVersion();\n\n    if (browserInfo.name === 'IE') {\n      const div = document.getElementById('protect-browser');\n      div.innerText = 'Sorry, this site does not support Internet Explorer. In order to avoid affecting the normal use of our features, please use a more modern browser such as Edge, Firefox, Chrome, or Safari.'\n    } else {\n      const notSupport = defaultList.some(item => {\n        if (item.name === browserInfo.name) {\n          return compareVersion(browserInfo.version, item.version) === -1;\n        }\n        return false;\n      });\n      if (notSupport) {\n        const div = document.getElementById('protect-browser');\n        div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'\n      }\n    }\n\n  </script>\n</html>\n"
  },
  {
    "path": "ui/public/manifest.json",
    "content": "{\n  \"short_name\": \"Answer\",\n  \"name\": \"Apache Answer\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "ui/public/robots.txt",
    "content": "====\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n====\n\n# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "ui/scripts/env.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst yaml = require('js-yaml');\n\nconst configFilePath = path.resolve(__dirname, '../../configs/config.yaml');\nconst envFilePath = path.resolve(__dirname, '../.env.production');\n\n// Read config.yaml file\nconst config = yaml.load(fs.readFileSync(configFilePath, 'utf8'));\n\n// Generate .env file content\nlet envContent = 'TSC_COMPILE_ON_ERROR=true\\nESLINT_NO_DEV_ERRORS=true\\n';\nfor (const key in config.ui) {\n  const value = config.ui[key];\n  envContent += `${key !== 'public_url' ? 'REACT_APP_' : ''}${key.toUpperCase()}=${value}\\n`;\n}\n\n// Write .env file\nfs.writeFileSync(envFilePath, envContent);\n"
  },
  {
    "path": "ui/scripts/importPlugins.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst path = require('path');\nconst fs = require('fs');\n\nconst pluginPath = path.join(__dirname, '../src/plugins');\nconst pluginFolders = fs.readdirSync(pluginPath);\n\nfunction resetPackageJson() {\n    const packageJsonPath = path.join(__dirname, '..', 'package.json');\n    const packageJsonContent = require(packageJsonPath);\n    const dependencies = packageJsonContent.dependencies;\n    for (const key in dependencies) {\n        if (dependencies[key].startsWith('workspace')) {\n            delete dependencies[key];\n        }\n    }\n    fs.writeFileSync(\n        packageJsonPath,\n        JSON.stringify(packageJsonContent, null, 2),\n    );\n}\n\nfunction addPluginToPackageJson(packageName) {\n    const packageJsonPath = path.join(__dirname, '..', 'package.json');\n    const packageJsonContent = require(packageJsonPath);\n    packageJsonContent.dependencies[packageName] = 'workspace:*';\n\n    fs.writeFileSync(\n        packageJsonPath,\n        JSON.stringify(packageJsonContent, null, 2),\n    );\n}\n\n\nresetPackageJson();\n\npluginFolders.forEach((folder) => {\n    const pluginFolder = path.join(pluginPath, folder);\n    const stat = fs.statSync(pluginFolder);\n\n    if (stat.isDirectory() && folder !== 'builtin') {\n        if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) {\n            return;\n        }\n        const packageJson = require(path.join(pluginFolder, 'package.json'));\n        const packageName = packageJson.name;\n\n        addPluginToPackageJson(packageName);\n    }\n});"
  },
  {
    "path": "ui/scripts/loadPlugins.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst path = require('path');\nconst fs = require('fs');\nconst yaml = require('js-yaml');\n\nconst pluginPath = path.join(__dirname, '../src/plugins');\nconst pluginFolders = fs.readdirSync(pluginPath);\n\nfunction pascalize(str) {\n  return str.split(/[_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');\n}\n\nfunction resetIndexTs() {\n  const indexTsPath = path.join(pluginPath, 'index.ts');\n  fs.writeFileSync(indexTsPath, '');\n}\n\nfunction addPluginToIndexTs(packageName, pluginFolder) {\n  const indexTsPath = path.join(pluginPath, 'index.ts');\n  const indexTsContent = fs.readFileSync(indexTsPath, 'utf-8');\n  const lines = indexTsContent.split('\\n');\n  const ComponentName = pascalize(packageName);\n\n  const importLine = `const load${ComponentName} = () => import('${packageName}').then(module => module.default);`;\n  const info = yaml.load(fs.readFileSync(path.join(pluginFolder, 'info.yaml'), 'utf8'));\n  const exportLine = `export const ${info.slug_name} = load${ComponentName}`;\n\n  if (!lines.includes(exportLine)) {\n    lines.push(importLine);\n    lines.push(exportLine);\n  }\n\n  fs.writeFileSync(indexTsPath, lines.join('\\n'));\n}\n\nconst pluginLength = pluginFolders.filter((folder) => {\n  const pluginFolder = path.join(pluginPath, folder);\n  const stat = fs.statSync(pluginFolder);\n  return stat.isDirectory() && folder !== 'builtin';\n}).length;\n\nif (pluginLength > 0) {\n  resetIndexTs();\n}\n\npluginFolders.forEach((folder) => {\n  const pluginFolder = path.join(pluginPath, folder);\n  const stat = fs.statSync(pluginFolder);\n\n  if (stat.isDirectory() && folder !== 'builtin') {\n    if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) {\n      return;\n    }\n    const packageJson = require(path.join(pluginFolder, 'package.json'));\n    const packageName = packageJson.name;\n\n    addPluginToIndexTs(packageName, pluginFolder);\n  }\n});\n\n"
  },
  {
    "path": "ui/scripts/preinstall.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed.\n\nrequire('./loadPlugins');\n\nif (!/pnpm/.test(process.env.npm_execpath)) {\n  console.warn(\n    `\\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\\u001b[39m\\n`,\n  );\n  process.exit(1);\n}\n"
  },
  {
    "path": "ui/scripts/setup-eslint.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst UI_DIR = path.resolve(__dirname, '..');  // UI directory\nconst ROOT_DIR = path.resolve(UI_DIR, '..');   // Project root directory\nconst GIT_DIR = getGitDir(ROOT_DIR);           // Git root directory\nconst HUSKY_DIR = path.join(GIT_DIR, '.husky');\n\n// Find Git directory\nfunction getGitDir(startDir) {\n  let currentDir = startDir;\n\n  while (currentDir !== path.parse(currentDir).root) {\n    const gitDir = path.join(currentDir, '.git');\n    if (fs.existsSync(gitDir)) {\n      return currentDir;\n    }\n    currentDir = path.dirname(currentDir);\n  }\n\n  throw new Error('Could not find Git directory');\n}\n\n\nif (!fs.existsSync(HUSKY_DIR)) {\n  console.log(`Creating husky directory: ${HUSKY_DIR}`);\n  fs.mkdirSync(HUSKY_DIR, { recursive: true });\n}\n\nif (!fs.existsSync(path.join(HUSKY_DIR, '_'))) {\n  console.log(`Creating husky _ directory: ${path.join(HUSKY_DIR, '_')}`);\n  fs.mkdirSync(path.join(HUSKY_DIR, '_'), { recursive: true });\n}\n\n// init husky\ntry {\n  console.log('Initializing husky...');\n  execSync('npx husky install', { cwd: GIT_DIR, stdio: 'inherit' });\n} catch (error) {\n  console.error(`❌ Failed to initialize husky: ${error.message}`);\n  process.exit(1);\n}\n\n// create lint-staged config file\nconst lintStagedConfig = {\n  \"src/**/*.{ts,tsx}\": [\n    \"eslint --fix\",\n    \"prettier --write\"\n  ],\n  \"src/**/*.{scss,md}\": [\n    \"prettier --write\"\n  ]\n};\n\nconsole.log(`Creating lint-staged config: ${path.join(UI_DIR, '.lintstagedrc.json')}`);\nfs.writeFileSync(\n  path.join(UI_DIR, '.lintstagedrc.json'),\n  JSON.stringify(lintStagedConfig, null, 2)\n);\n\n// create pre-commit hook\nconst preCommitContent = `#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\necho \"🔍 Start running the code check...\"\n\n# Getting the Git Root Directory\nGIT_ROOT=$(git rev-parse --show-toplevel)\n\n# Get a list of staging files\nSTAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)\n\n# Check for files in the ui/ directory\nUI_FILES=$(echo \"$STAGED_FILES\" | grep '^ui/' || echo \"\")\n\nif [ -n \"$UI_FILES\" ]; then\n  echo \"🔎 Discover ui file changes, run code checks...\"\n\n  # Switch to the ui directory\n  cd \"$GIT_ROOT/ui\" || {\n    echo \"❌ Unable to access the UI catalog\"\n    exit 1\n  }\n\n  # 运行 lint-staged\n  echo \"✨ Running ESLint and Prettier Formatting...\"\n  npx lint-staged --verbose\n\n  LINT_STAGED_RESULT=$?\n  if [ $LINT_STAGED_RESULT -ne 0 ]; then\n    echo \"❌ Code check failed, please fix the above problem\"\n    exit $LINT_STAGED_RESULT\n  fi\n\n  echo \"✅ Code check passes！\"\nelse\n  echo \"ℹ️ No front-end file changes found, skip code checking\"\nfi\n\necho \"🎉 Pre-submission check completed\"\n`;\n\nconsole.log(`Creating pre-commit hook: ${path.join(HUSKY_DIR, 'pre-commit')}`);\nfs.writeFileSync(path.join(HUSKY_DIR, 'pre-commit'), preCommitContent);\nexecSync(`chmod +x ${path.join(HUSKY_DIR, 'pre-commit')}`);\n\n// create husky.sh\nconst huskyShContent = `#!/bin/sh\nif [ -z \"$husky_skip_init\" ]; then\n  debug () {\n    if [ \"$HUSKY_DEBUG\" = \"1\" ]; then\n      echo \"husky (debug) - $1\"\n    fi\n  }\n\n  readonly hook_name=\"$(basename \"$0\")\"\n  debug \"starting $hook_name...\"\n\n  if [ \"$HUSKY\" = \"0\" ]; then\n    debug \"HUSKY=0, skip hook\"\n    exit 0\n  fi\n\n  if [ -f ~/.huskyrc ]; then\n    debug \"sourcing ~/.huskyrc\"\n    . ~/.huskyrc\n  fi\n\n  export readonly husky_skip_init=1\n  sh -e \"$0\" \"$@\"\n  exitCode=\"$?\"\n\n  if [ $exitCode != 0 ]; then\n    echo \"husky - $hook_name hook exited with code $exitCode (error)\"\n  fi\n\n  exit $exitCode\nfi\n`;\n\nconsole.log(`Creating husky.sh: ${path.join(HUSKY_DIR, '_', 'husky.sh')}`);\nfs.writeFileSync(\n  path.join(HUSKY_DIR, '_', 'husky.sh'),\n  huskyShContent\n);\nexecSync(`chmod +x ${path.join(HUSKY_DIR, '_', 'husky.sh')}`);\n\nconsole.log('Lint setup complete! Husky and lint-staged have been configured.');\nconsole.log(`Git root directory: ${GIT_DIR}`);\nconsole.log(`Husky directory: ${HUSKY_DIR}`);\n"
  },
  {
    "path": "ui/src/App.test.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React from 'react';\n\nimport { render, screen } from '@testing-library/react';\n\nimport App from './App';\n\ntest('renders learn react link', () => {\n  render(<App />);\n  const linkElement = screen.getByText(/learn react/i);\n  expect(linkElement).toBeInTheDocument();\n});\n"
  },
  {
    "path": "ui/src/App.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { RouterProvider, createBrowserRouter } from 'react-router-dom';\n\nimport './i18n/init';\n\nimport '@/utils/pluginKit';\nimport { useMergeRoutes } from '@/router';\nimport InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder';\n\nfunction App() {\n  const routes = useMergeRoutes();\n  if (routes.length === 0) {\n    return <InitialLoadingPlaceholder />;\n  }\n  const router = createBrowserRouter(routes, {\n    basename: process.env.REACT_APP_BASE_URL,\n  });\n  return <RouterProvider router={router} />;\n}\n\nexport default App;\n"
  },
  {
    "path": "ui/src/behaviour/useLegalClick.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { MouseEvent, useCallback } from 'react';\n\nimport { useLegalPrivacy, useLegalTos } from '@/services/client/legal';\n\nexport const useLegalClick = () => {\n  const { data: tos } = useLegalTos();\n  const { data: privacy } = useLegalPrivacy();\n\n  const legalClick = useCallback(\n    (evt: MouseEvent, type: 'tos' | 'privacy') => {\n      evt.stopPropagation();\n      const contentText =\n        type === 'tos'\n          ? tos?.terms_of_service_original_text\n          : privacy?.privacy_policy_original_text;\n      let matchUrl: URL | undefined;\n      try {\n        if (contentText) {\n          matchUrl = new URL(contentText);\n        }\n        // eslint-disable-next-line no-empty\n      } catch (ex) {}\n      if (matchUrl) {\n        evt.preventDefault();\n        window.open(matchUrl.toString());\n      }\n    },\n    [tos, privacy],\n  );\n\n  return legalClick;\n};\n"
  },
  {
    "path": "ui/src/common/_variable.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n$link-hover-decoration: none;\n$enable-negative-margins: true;\n$blue: #0033ff !default;\n$placeholder-opacity-max: 0.2;\n$placeholder-opacity-min: 0.1;\n$enable-smooth-scroll: false;\n"
  },
  {
    "path": "ui/src/common/color.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n:root {\n  --an-side-nav-link: rgba(0, 0, 0, 0.65);\n  --an-toolbar-divider: rgba(0, 0, 0, 0.1);\n  --an-ced4da: #ced4da;\n  --an-e9ecef: #e9ecef;\n  --an-pre: #f8f9fa;\n  --an-6c757d: #6c757d;\n  --an-212529: #212529;\n  --an-gray-300: var(--bs-gray-300);\n  --an-white: #fff;\n  --an-inbox-warning: #fff3cd80;\n  --an-f5: #f5f5f5;\n  --an-answer-item-border-top: rgba(0, 0, 0, 0.125);\n  --an-answer-inbox-nav-border-top: var(--bs-border-color);\n  --an-comment-item-border-bottom: var(--bs-colors-gray-200, #e9ecef);\n  --an-editor-toolbar-hover: #f8f9fa;\n  --ans-editor-toolbar-focus: #dae0e5;\n  --an-editor-placeholder-color: #6c757d;\n  --an-side-nav-link-hover-color: rgba(0, 0, 0, 0.85);\n  --an-invite-answer-item-active: #e9ecef;\n  --an-alert-exist-color: #055160;\n}\n\n[data-bs-theme='dark'] {\n  --an-side-nav-link: rgba(255, 255, 255, 0.65);\n  --an-toolbar-divider: rgba(255, 255, 255, 0.3);\n  --an-ced4da: var(--bs-border-color);\n  --an-e9ecef: #161b22;\n  --an-pre: #161b22;\n  --an-6c757d: var(--bs-body-color);\n  --an-212529: var(--bs-body-color);\n  --an-gray-300: #161b22;\n  --an-white: #000;\n  --an-inbox-warning: #38363180;\n  --an-f5: var(--bs-body-bg);\n  --an-answer-item-border-top: var(--bs-border-color);\n  --an-answer-inbox-nav-border-top: var(--bs-border-color);\n  --an-comment-item-border-bottom: var(--bs-border-color);\n  --an-editor-toolbar-hover: var(--bs-tertiary-bg);\n  --ans-editor-toolbar-focus: var(--bs-tertiary-bg);\n  --an-editor-placeholder-color: var(--bs-body-color);\n  --an-side-nav-link-hover-color: var(--bs-body-color);\n  --an-invite-answer-item-active: var(--bs-tertiary-bg);\n  --an-alert-exist-color: #60cee4;\n}\n\n[data-bs-theme='dark'] {\n  .link-dark {\n    color: rgba(var(--bs-emphasis-color-rgb), 0.8) !important;\n  }\n  /** CodeMirror **/\n\n  .cm-editor {\n    background: var(--bs-body-bg);\n    color: var(--bs-body-color);\n  }\n\n  .cm-cursor {\n    border-left-color: var(--bs-body-color);\n  }\n  .ͼ2.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {\n    background-color: #3e4451;\n  }\n  /** link color **/\n  .ͼc {\n    color: rgb(60, 138, 233);\n  }\n  .ͼ5 {\n    color: var(--bs-body-color);\n  }\n\n  .ͼ2 .cm-selectionBackground {\n    background: #3e4451;\n  }\n  /** CodeMirror end **/\n\n  .bg-light {\n    background-color: rgba(0, 0, 0, 0.5) !important;\n  }\n  .text-bg-dark {\n    color: #000 !important;\n    background-color: RGBA(255, 255, 255, var(--bs-bg-opacity, 1)) !important;\n  }\n  /** side nav **/\n  #sideNav,\n  #answerAccordion {\n    .nav-link:hover,\n    .nav-link.active {\n      background-color: #2b3035 !important;\n    }\n  }\n\n  /** tag **/\n  .badge-tag {\n    background: $gray-800;\n    color: $gray-300;\n    &:hover {\n      background: $gray-600;\n    }\n  }\n\n  .badge-tag-reserved {\n    color: $orange-200;\n    background: $orange-800;\n    &:hover {\n      background: $orange-700;\n    }\n  }\n\n  .view-level1 {\n    color: $orange-300;\n  }\n\n  .view-level2 {\n    color: $orange-200;\n  }\n\n  .view-level3 {\n    color: $orange-100;\n  }\n}\n"
  },
  {
    "path": "ui/src/common/constants.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport const DEFAULT_SITE_NAME = 'Answer';\nexport const DEFAULT_LANG = 'en_US';\nexport const CURRENT_LANG_STORAGE_KEY = '_a_lang_';\nexport const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';\nexport const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';\nexport const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';\nexport const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';\nexport const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';\nexport const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';\nexport const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';\nexport const DEFAULT_THEME = 'system';\nexport const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99;\nexport const SKELETON_SHOW_TIME = 1000;\nexport const LIST_VIEW_STORAGE_KEY = '_a_list_view_';\nexport const EXTERNAL_CONTENT_DISPLAY_MODE = '_a_ecd_';\n\nexport const USER_AGENT_NAMES = {\n  SegmentFault: 'SegmentFault',\n  WeChat: 'WeChat',\n  WeCom: 'WeCom',\n  DingTalk: 'DingTalk',\n};\n\nexport const ADMIN_LIST_STATUS = {\n  // normal;\n  1: {\n    variant: 'text-bg-success',\n    name: 'normal',\n  },\n  // closed;\n  2: {\n    variant: 'text-bg-warning',\n    name: 'closed',\n  },\n  // deleted\n  10: {\n    variant: 'text-bg-danger',\n    name: 'deleted',\n  },\n  // pending\n  11: {\n    variant: 'text-bg-warning',\n    name: 'pending',\n  },\n  normal: {\n    variant: 'text-bg-success',\n    name: 'normal',\n  },\n  closed: {\n    variant: 'text-bg-warning',\n    name: 'closed',\n  },\n  deleted: {\n    variant: 'text-bg-danger',\n    name: 'deleted',\n  },\n  pending: {\n    variant: 'text-bg-warning',\n    name: 'pending',\n  },\n  unlisted: {\n    variant: 'text-bg-secondary',\n    name: 'unlisted',\n  },\n};\n\n/**\n * ADMIN_NAV_MENUS is the navigation menu for the admin panel.\n * pathPrefix is used to activate the menu item when the activeKey starts with the pathPrefix.\n */\n\nexport const ADMIN_NAV_MENUS = [\n  {\n    name: 'dashboard',\n    icon: 'speedometer',\n    children: [],\n  },\n  {\n    name: 'contents',\n    icon: 'file-earmark-text-fill',\n    children: [\n      { name: 'questions', path: 'qa/questions', pathPrefix: 'qa/' },\n      { name: 'tags', path: 'tags/settings', pathPrefix: 'tags/' },\n    ],\n  },\n  {\n    name: 'intelligence',\n    icon: 'robot',\n    children: [\n      { name: 'ai_settings', path: 'ai-settings' },\n      { name: 'ai_assistant', path: 'ai-assistant' },\n      { name: 'mcp' },\n    ],\n  },\n  {\n    name: 'community',\n    icon: 'people-fill',\n    children: [\n      { name: 'users', pathPrefix: 'users/' },\n      { name: 'badges' },\n      { name: 'rules', path: 'rules/privileges', pathPrefix: 'rules/' },\n    ],\n  },\n  {\n    name: 'apperance',\n    icon: 'palette-fill',\n    children: [\n      {\n        name: 'themes',\n      },\n      {\n        name: 'customize',\n      },\n      { name: 'branding' },\n      { name: 'interface' },\n    ],\n  },\n  {\n    name: 'advanced',\n    icon: 'gear-fill',\n    children: [\n      { name: 'general' },\n      { name: 'security' },\n      { name: 'files' },\n      { name: 'login' },\n      { name: 'seo' },\n      { name: 'smtp' },\n      { name: 'apikeys' },\n    ],\n  },\n  {\n    name: 'plugins',\n    icon: 'plugin',\n    children: [\n      {\n        name: 'installed_plugins',\n        path: 'installed-plugins',\n      },\n    ],\n  },\n];\n\nexport const ADMIN_QA_NAV_MENUS = [\n  { name: 'questions', path: '/admin/qa/questions' },\n  { name: 'answers', path: '/admin/qa/answers' },\n  { name: 'settings', path: '/admin/qa/settings' },\n];\n\nexport const ADMIN_TAGS_NAV_MENUS = [\n  // { name: 'tags', path: '/admin/tags' },\n  {\n    name: 'settings',\n    path: '/admin/tags/settings',\n  },\n];\n\nexport const ADMIN_USERS_NAV_MENUS = [\n  { name: 'users', path: '/admin/users' },\n  { name: 'settings', path: '/admin/users/settings' },\n];\n\nexport const ADMIN_RULES_NAV_MENUS = [\n  { name: 'privileges', path: '/admin/rules/privileges' },\n  { name: 'policies', path: '/admin/rules/policies' },\n];\n\nexport const TIMEZONES = [\n  {\n    label: 'Africa',\n    options: [\n      { value: 'Africa/Abidjan', label: 'Abidjan' },\n      { value: 'Africa/Accra', label: 'Accra' },\n      { value: 'Africa/Addis_Ababa', label: 'Addis Ababa' },\n      { value: 'Africa/Algiers', label: 'Algiers' },\n      { value: 'Africa/Asmara', label: 'Asmara' },\n      { value: 'Africa/Bamako', label: 'Bamako' },\n      { value: 'Africa/Bangui', label: 'Bangui' },\n      { value: 'Africa/Banjul', label: 'Banjul' },\n      { value: 'Africa/Bissau', label: 'Bissau' },\n      { value: 'Africa/Blantyre', label: 'Blantyre' },\n      { value: 'Africa/Brazzaville', label: 'Brazzaville' },\n      { value: 'Africa/Bujumbura', label: 'Bujumbura' },\n      { value: 'Africa/Cairo', label: 'Cairo' },\n      { value: 'Africa/Casablanca', label: 'Casablanca' },\n      { value: 'Africa/Ceuta', label: 'Ceuta' },\n      { value: 'Africa/Conakry', label: 'Conakry' },\n      { value: 'Africa/Dakar', label: 'Dakar' },\n      { value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' },\n      { value: 'Africa/Djibouti', label: 'Djibouti' },\n      { value: 'Africa/Douala', label: 'Douala' },\n      { value: 'Africa/El_Aaiun', label: 'El Aaiun' },\n      { value: 'Africa/Freetown', label: 'Freetown' },\n      { value: 'Africa/Gaborone', label: 'Gaborone' },\n      { value: 'Africa/Harare', label: 'Harare' },\n      { value: 'Africa/Johannesburg', label: 'Johannesburg' },\n      { value: 'Africa/Juba', label: 'Juba' },\n      { value: 'Africa/Kampala', label: 'Kampala' },\n      { value: 'Africa/Khartoum', label: 'Khartoum' },\n      { value: 'Africa/Kigali', label: 'Kigali' },\n      { value: 'Africa/Kinshasa', label: 'Kinshasa' },\n      { value: 'Africa/Lagos', label: 'Lagos' },\n      { value: 'Africa/Libreville', label: 'Libreville' },\n      { value: 'Africa/Lome', label: 'Lome' },\n      { value: 'Africa/Luanda', label: 'Luanda' },\n      { value: 'Africa/Lubumbashi', label: 'Lubumbashi' },\n      { value: 'Africa/Lusaka', label: 'Lusaka' },\n      { value: 'Africa/Malabo', label: 'Malabo' },\n      { value: 'Africa/Maputo', label: 'Maputo' },\n      { value: 'Africa/Maseru', label: 'Maseru' },\n      { value: 'Africa/Mbabane', label: 'Mbabane' },\n      { value: 'Africa/Mogadishu', label: 'Mogadishu' },\n      { value: 'Africa/Monrovia', label: 'Monrovia' },\n      { value: 'Africa/Nairobi', label: 'Nairobi' },\n      { value: 'Africa/Ndjamena', label: 'Ndjamena' },\n      { value: 'Africa/Niamey', label: 'Niamey' },\n      { value: 'Africa/Nouakchott', label: 'Nouakchott' },\n      { value: 'Africa/Ouagadougou', label: 'Ouagadougou' },\n      { value: 'Africa/Porto-Novo', label: 'Porto-Novo' },\n      { value: 'Africa/Sao_Tome', label: 'Sao Tome' },\n      { value: 'Africa/Tripoli', label: 'Tripoli' },\n      { value: 'Africa/Tunis', label: 'Tunis' },\n      { value: 'Africa/Windhoek', label: 'Windhoek' },\n    ],\n  },\n  {\n    label: 'America',\n    options: [\n      { value: 'America/Adak', label: 'Adak' },\n      { value: 'America/Anchorage', label: 'Anchorage' },\n      { value: 'America/Anguilla', label: 'Anguilla' },\n      { value: 'America/Antigua', label: 'Antigua' },\n      { value: 'America/Araguaina', label: 'Araguaina' },\n      {\n        value: 'America/Argentina/Buenos_Aires',\n        label: 'Argentina - Buenos Aires',\n      },\n      { value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' },\n      { value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' },\n      { value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' },\n      { value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' },\n      { value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' },\n      {\n        value: 'America/Argentina/Rio_Gallegos',\n        label: 'Argentina - Rio Gallegos',\n      },\n      { value: 'America/Argentina/Salta', label: 'Argentina - Salta' },\n      { value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' },\n      { value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' },\n      { value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' },\n      { value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' },\n      { value: 'America/Aruba', label: 'Aruba' },\n      { value: 'America/Asuncion', label: 'Asuncion' },\n      { value: 'America/Atikokan', label: 'Atikokan' },\n      { value: 'America/Bahia', label: 'Bahia' },\n      { value: 'America/Bahia_Banderas', label: 'Bahia Banderas' },\n      { value: 'America/Barbados', label: 'Barbados' },\n      { value: 'America/Belem', label: 'Belem' },\n      { value: 'America/Belize', label: 'Belize' },\n      { value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' },\n      { value: 'America/Boa_Vista', label: 'Boa Vista' },\n      { value: 'America/Bogota', label: 'Bogota' },\n      { value: 'America/Boise', label: 'Boise' },\n      { value: 'America/Cambridge_Bay', label: 'Cambridge Bay' },\n      { value: 'America/Campo_Grande', label: 'Campo Grande' },\n      { value: 'America/Cancun', label: 'Cancun' },\n      { value: 'America/Caracas', label: 'Caracas' },\n      { value: 'America/Cayenne', label: 'Cayenne' },\n      { value: 'America/Cayman', label: 'Cayman' },\n      { value: 'America/Chicago', label: 'Chicago' },\n      { value: 'America/Chihuahua', label: 'Chihuahua' },\n      { value: 'America/Costa_Rica', label: 'Costa Rica' },\n      { value: 'America/Creston', label: 'Creston' },\n      { value: 'America/Cuiaba', label: 'Cuiaba' },\n      { value: 'America/Curacao', label: 'Curacao' },\n      { value: 'America/Danmarkshavn', label: 'Danmarkshavn' },\n      { value: 'America/Dawson', label: 'Dawson' },\n      { value: 'America/Dawson_Creek', label: 'Dawson Creek' },\n      { value: 'America/Denver', label: 'Denver' },\n      { value: 'America/Detroit', label: 'Detroit' },\n      { value: 'America/Dominica', label: 'Dominica' },\n      { value: 'America/Edmonton', label: 'Edmonton' },\n      { value: 'America/Eirunepe', label: 'Eirunepe' },\n      { value: 'America/El_Salvador', label: 'El Salvador' },\n      { value: 'America/Fort_Nelson', label: 'Fort Nelson' },\n      { value: 'America/Fortaleza', label: 'Fortaleza' },\n      { value: 'America/Glace_Bay', label: 'Glace Bay' },\n      { value: 'America/Godthab', label: 'Godthab' },\n      { value: 'America/Goose_Bay', label: 'Goose Bay' },\n      { value: 'America/Grand_Turk', label: 'Grand Turk' },\n      { value: 'America/Grenada', label: 'Grenada' },\n      { value: 'America/Guadeloupe', label: 'Guadeloupe' },\n      { value: 'America/Guatemala', label: 'Guatemala' },\n      { value: 'America/Guayaquil', label: 'Guayaquil' },\n      { value: 'America/Guyana', label: 'Guyana' },\n      { value: 'America/Halifax', label: 'Halifax' },\n      { value: 'America/Havana', label: 'Havana' },\n      { value: 'America/Hermosillo', label: 'Hermosillo' },\n      {\n        value: 'America/Indiana/Indianapolis',\n        label: 'Indiana - Indianapolis',\n      },\n      { value: 'America/Indiana/Knox', label: 'Indiana - Knox' },\n      { value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' },\n      { value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' },\n      { value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' },\n      { value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' },\n      { value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' },\n      { value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' },\n      { value: 'America/Inuvik', label: 'Inuvik' },\n      { value: 'America/Iqaluit', label: 'Iqaluit' },\n      { value: 'America/Jamaica', label: 'Jamaica' },\n      { value: 'America/Juneau', label: 'Juneau' },\n      { value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' },\n      { value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' },\n      { value: 'America/Kralendijk', label: 'Kralendijk' },\n      { value: 'America/La_Paz', label: 'La Paz' },\n      { value: 'America/Lima', label: 'Lima' },\n      { value: 'America/Los_Angeles', label: 'Los Angeles' },\n      { value: 'America/Lower_Princes', label: 'Lower Princes' },\n      { value: 'America/Maceio', label: 'Maceio' },\n      { value: 'America/Managua', label: 'Managua' },\n      { value: 'America/Manaus', label: 'Manaus' },\n      { value: 'America/Marigot', label: 'Marigot' },\n      { value: 'America/Martinique', label: 'Martinique' },\n      { value: 'America/Matamoros', label: 'Matamoros' },\n      { value: 'America/Mazatlan', label: 'Mazatlan' },\n      { value: 'America/Miquelon', label: 'Miquelon' },\n      { value: 'America/Moncton', label: 'Moncton' },\n      { value: 'America/Monterrey', label: 'Monterrey' },\n      { value: 'America/Montevideo', label: 'Montevideo' },\n      { value: 'America/Montserrat', label: 'Montserrat' },\n      { value: 'America/Nassau', label: 'Nassau' },\n      { value: 'America/New_York', label: 'New York' },\n      { value: 'America/Nipigon', label: 'Nipigon' },\n      { value: 'America/Nome', label: 'Nome' },\n      { value: 'America/Noronha', label: 'Noronha' },\n      { value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' },\n      { value: 'America/North_Dakota/Center', label: 'North Dakota - Center' },\n      {\n        value: 'America/North_Dakota/New_Salem',\n        label: 'North Dakota - New Salem',\n      },\n      { value: 'America/Ojinaga', label: 'Ojinaga' },\n      { value: 'America/Panama', label: 'Panama' },\n      { value: 'America/Pangnirtung', label: 'Pangnirtung' },\n      { value: 'America/Paramaribo', label: 'Paramaribo' },\n      { value: 'America/Phoenix', label: 'Phoenix' },\n      { value: 'America/Port-au-Prince', label: 'Port-au-Prince' },\n      { value: 'America/Port_of_Spain', label: 'Port of Spain' },\n      { value: 'America/Porto_Velho', label: 'Porto Velho' },\n      { value: 'America/Puerto_Rico', label: 'Puerto Rico' },\n      { value: 'America/Punta_Arenas', label: 'Punta Arenas' },\n      { value: 'America/Rainy_River', label: 'Rainy River' },\n      { value: 'America/Rankin_Inlet', label: 'Rankin Inlet' },\n      { value: 'America/Recife', label: 'Recife' },\n      { value: 'America/Regina', label: 'Regina' },\n      { value: 'America/Resolute', label: 'Resolute' },\n      { value: 'America/Rio_Branco', label: 'Rio Branco' },\n      { value: 'America/Santarem', label: 'Santarem' },\n      { value: 'America/Santiago', label: 'Santiago' },\n      { value: 'America/Santo_Domingo', label: 'Santo Domingo' },\n      { value: 'America/Sao_Paulo', label: 'Sao Paulo' },\n      { value: 'America/Scoresbysund', label: 'Scoresbysund' },\n      { value: 'America/Sitka', label: 'Sitka' },\n      { value: 'America/St_Barthelemy', label: 'St Barthelemy' },\n      { value: 'America/St_Johns', label: 'St Johns' },\n      { value: 'America/St_Kitts', label: 'St Kitts' },\n      { value: 'America/St_Lucia', label: 'St Lucia' },\n      { value: 'America/St_Thomas', label: 'St Thomas' },\n      { value: 'America/St_Vincent', label: 'St Vincent' },\n      { value: 'America/Swift_Current', label: 'Swift Current' },\n      { value: 'America/Tegucigalpa', label: 'Tegucigalpa' },\n      { value: 'America/Thule', label: 'Thule' },\n      { value: 'America/Thunder_Bay', label: 'Thunder Bay' },\n      { value: 'America/Tijuana', label: 'Tijuana' },\n      { value: 'America/Toronto', label: 'Toronto' },\n      { value: 'America/Tortola', label: 'Tortola' },\n      { value: 'America/Vancouver', label: 'Vancouver' },\n      { value: 'America/Whitehorse', label: 'Whitehorse' },\n      { value: 'America/Winnipeg', label: 'Winnipeg' },\n      { value: 'America/Yakutat', label: 'Yakutat' },\n      { value: 'America/Yellowknife', label: 'Yellowknife' },\n    ],\n  },\n  {\n    label: 'Antarctica',\n    options: [\n      { value: 'Antarctica/Casey', label: 'Casey' },\n      { value: 'Antarctica/Davis', label: 'Davis' },\n      { value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' },\n      { value: 'Antarctica/Macquarie', label: 'Macquarie' },\n      { value: 'Antarctica/Mawson', label: 'Mawson' },\n      { value: 'Antarctica/McMurdo', label: 'McMurdo' },\n      { value: 'Antarctica/Palmer', label: 'Palmer' },\n      { value: 'Antarctica/Rothera', label: 'Rothera' },\n      { value: 'Antarctica/Syowa', label: 'Syowa' },\n      { value: 'Antarctica/Troll', label: 'Troll' },\n      { value: 'Antarctica/Vostok', label: 'Vostok' },\n    ],\n  },\n  {\n    label: 'Arctic',\n    options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }],\n  },\n  {\n    label: 'Asia',\n    options: [\n      { value: 'Asia/Aden', label: 'Aden' },\n      { value: 'Asia/Almaty', label: 'Almaty' },\n      { value: 'Asia/Amman', label: 'Amman' },\n      { value: 'Asia/Anadyr', label: 'Anadyr' },\n      { value: 'Asia/Aqtau', label: 'Aqtau' },\n      { value: 'Asia/Aqtobe', label: 'Aqtobe' },\n      { value: 'Asia/Ashgabat', label: 'Ashgabat' },\n      { value: 'Asia/Atyrau', label: 'Atyrau' },\n      { value: 'Asia/Baghdad', label: 'Baghdad' },\n      { value: 'Asia/Bahrain', label: 'Bahrain' },\n      { value: 'Asia/Baku', label: 'Baku' },\n      { value: 'Asia/Bangkok', label: 'Bangkok' },\n      { value: 'Asia/Barnaul', label: 'Barnaul' },\n      { value: 'Asia/Beirut', label: 'Beirut' },\n      { value: 'Asia/Bishkek', label: 'Bishkek' },\n      { value: 'Asia/Brunei', label: 'Brunei' },\n      { value: 'Asia/Chita', label: 'Chita' },\n      { value: 'Asia/Choibalsan', label: 'Choibalsan' },\n      { value: 'Asia/Colombo', label: 'Colombo' },\n      { value: 'Asia/Damascus', label: 'Damascus' },\n      { value: 'Asia/Dhaka', label: 'Dhaka' },\n      { value: 'Asia/Dili', label: 'Dili' },\n      { value: 'Asia/Dubai', label: 'Dubai' },\n      { value: 'Asia/Dushanbe', label: 'Dushanbe' },\n      { value: 'Asia/Famagusta', label: 'Famagusta' },\n      { value: 'Asia/Gaza', label: 'Gaza' },\n      { value: 'Asia/Hebron', label: 'Hebron' },\n      { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' },\n      { value: 'Asia/Hong_Kong', label: 'Hong Kong' },\n      { value: 'Asia/Hovd', label: 'Hovd' },\n      { value: 'Asia/Irkutsk', label: 'Irkutsk' },\n      { value: 'Asia/Jakarta', label: 'Jakarta' },\n      { value: 'Asia/Jayapura', label: 'Jayapura' },\n      { value: 'Asia/Jerusalem', label: 'Jerusalem' },\n      { value: 'Asia/Kabul', label: 'Kabul' },\n      { value: 'Asia/Kamchatka', label: 'Kamchatka' },\n      { value: 'Asia/Karachi', label: 'Karachi' },\n      { value: 'Asia/Kathmandu', label: 'Kathmandu' },\n      { value: 'Asia/Khandyga', label: 'Khandyga' },\n      { value: 'Asia/Kolkata', label: 'Kolkata' },\n      { value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' },\n      { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' },\n      { value: 'Asia/Kuching', label: 'Kuching' },\n      { value: 'Asia/Kuwait', label: 'Kuwait' },\n      { value: 'Asia/Macau', label: 'Macau' },\n      { value: 'Asia/Magadan', label: 'Magadan' },\n      { value: 'Asia/Makassar', label: 'Makassar' },\n      { value: 'Asia/Manila', label: 'Manila' },\n      { value: 'Asia/Muscat', label: 'Muscat' },\n      { value: 'Asia/Nicosia', label: 'Nicosia' },\n      { value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' },\n      { value: 'Asia/Novosibirsk', label: 'Novosibirsk' },\n      { value: 'Asia/Omsk', label: 'Omsk' },\n      { value: 'Asia/Oral', label: 'Oral' },\n      { value: 'Asia/Phnom_Penh', label: 'Phnom Penh' },\n      { value: 'Asia/Pontianak', label: 'Pontianak' },\n      { value: 'Asia/Pyongyang', label: 'Pyongyang' },\n      { value: 'Asia/Qatar', label: 'Qatar' },\n      { value: 'Asia/Qostanay', label: 'Qostanay' },\n      { value: 'Asia/Qyzylorda', label: 'Qyzylorda' },\n      { value: 'Asia/Riyadh', label: 'Riyadh' },\n      { value: 'Asia/Sakhalin', label: 'Sakhalin' },\n      { value: 'Asia/Samarkand', label: 'Samarkand' },\n      { value: 'Asia/Seoul', label: 'Seoul' },\n      { value: 'Asia/Shanghai', label: 'Shanghai' },\n      { value: 'Asia/Singapore', label: 'Singapore' },\n      { value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' },\n      { value: 'Asia/Taipei', label: 'Taipei' },\n      { value: 'Asia/Tashkent', label: 'Tashkent' },\n      { value: 'Asia/Tbilisi', label: 'Tbilisi' },\n      { value: 'Asia/Tehran', label: 'Tehran' },\n      { value: 'Asia/Thimphu', label: 'Thimphu' },\n      { value: 'Asia/Tokyo', label: 'Tokyo' },\n      { value: 'Asia/Tomsk', label: 'Tomsk' },\n      { value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' },\n      { value: 'Asia/Urumqi', label: 'Urumqi' },\n      { value: 'Asia/Ust-Nera', label: 'Ust-Nera' },\n      { value: 'Asia/Vientiane', label: 'Vientiane' },\n      { value: 'Asia/Vladivostok', label: 'Vladivostok' },\n      { value: 'Asia/Yakutsk', label: 'Yakutsk' },\n      { value: 'Asia/Yangon', label: 'Yangon' },\n      { value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' },\n      { value: 'Asia/Yerevan', label: 'Yerevan' },\n    ],\n  },\n  {\n    label: 'Atlantic',\n    options: [\n      { value: 'Atlantic/Azores', label: 'Azores' },\n      { value: 'Atlantic/Bermuda', label: 'Bermuda' },\n      { value: 'Atlantic/Canary', label: 'Canary' },\n      { value: 'Atlantic/Cape_Verde', label: 'Cape Verde' },\n      { value: 'Atlantic/Faroe', label: 'Faroe' },\n      { value: 'Atlantic/Madeira', label: 'Madeira' },\n      { value: 'Atlantic/Reykjavik', label: 'Reykjavik' },\n      { value: 'Atlantic/South_Georgia', label: 'South Georgia' },\n      { value: 'Atlantic/Stanley', label: 'Stanley' },\n      { value: 'Atlantic/St_Helena', label: 'St Helena' },\n    ],\n  },\n  {\n    label: 'Australia',\n    options: [\n      { value: 'Australia/Adelaide', label: 'Adelaide' },\n      { value: 'Australia/Brisbane', label: 'Brisbane' },\n      { value: 'Australia/Broken_Hill', label: 'Broken Hill' },\n      { value: 'Australia/Currie', label: 'Currie' },\n      { value: 'Australia/Darwin', label: 'Darwin' },\n      { value: 'Australia/Eucla', label: 'Eucla' },\n      { value: 'Australia/Hobart', label: 'Hobart' },\n      { value: 'Australia/Lindeman', label: 'Lindeman' },\n      { value: 'Australia/Lord_Howe', label: 'Lord Howe' },\n      { value: 'Australia/Melbourne', label: 'Melbourne' },\n      { value: 'Australia/Perth', label: 'Perth' },\n      { value: 'Australia/Sydney', label: 'Sydney' },\n    ],\n  },\n  {\n    label: 'Europe',\n    options: [\n      { value: 'Europe/Amsterdam', label: 'Amsterdam' },\n      { value: 'Europe/Andorra', label: 'Andorra' },\n      { value: 'Europe/Astrakhan', label: 'Astrakhan' },\n      { value: 'Europe/Athens', label: 'Athens' },\n      { value: 'Europe/Belgrade', label: 'Belgrade' },\n      { value: 'Europe/Berlin', label: 'Berlin' },\n      { value: 'Europe/Bratislava', label: 'Bratislava' },\n      { value: 'Europe/Brussels', label: 'Brussels' },\n      { value: 'Europe/Bucharest', label: 'Bucharest' },\n      { value: 'Europe/Budapest', label: 'Budapest' },\n      { value: 'Europe/Busingen', label: 'Busingen' },\n      { value: 'Europe/Chisinau', label: 'Chisinau' },\n      { value: 'Europe/Copenhagen', label: 'Copenhagen' },\n      { value: 'Europe/Dublin', label: 'Dublin' },\n      { value: 'Europe/Gibraltar', label: 'Gibraltar' },\n      { value: 'Europe/Guernsey', label: 'Guernsey' },\n      { value: 'Europe/Helsinki', label: 'Helsinki' },\n      { value: 'Europe/Isle_of_Man', label: 'Isle of Man' },\n      { value: 'Europe/Istanbul', label: 'Istanbul' },\n      { value: 'Europe/Jersey', label: 'Jersey' },\n      { value: 'Europe/Kaliningrad', label: 'Kaliningrad' },\n      { value: 'Europe/Kiev', label: 'Kiev' },\n      { value: 'Europe/Kirov', label: 'Kirov' },\n      { value: 'Europe/Lisbon', label: 'Lisbon' },\n      { value: 'Europe/Ljubljana', label: 'Ljubljana' },\n      { value: 'Europe/London', label: 'London' },\n      { value: 'Europe/Luxembourg', label: 'Luxembourg' },\n      { value: 'Europe/Madrid', label: 'Madrid' },\n      { value: 'Europe/Malta', label: 'Malta' },\n      { value: 'Europe/Mariehamn', label: 'Mariehamn' },\n      { value: 'Europe/Minsk', label: 'Minsk' },\n      { value: 'Europe/Monaco', label: 'Monaco' },\n      { value: 'Europe/Moscow', label: 'Moscow' },\n      { value: 'Europe/Oslo', label: 'Oslo' },\n      { value: 'Europe/Paris', label: 'Paris' },\n      { value: 'Europe/Podgorica', label: 'Podgorica' },\n      { value: 'Europe/Prague', label: 'Prague' },\n      { value: 'Europe/Riga', label: 'Riga' },\n      { value: 'Europe/Rome', label: 'Rome' },\n      { value: 'Europe/Samara', label: 'Samara' },\n      { value: 'Europe/San_Marino', label: 'San Marino' },\n      { value: 'Europe/Sarajevo', label: 'Sarajevo' },\n      { value: 'Europe/Saratov', label: 'Saratov' },\n      { value: 'Europe/Simferopol', label: 'Simferopol' },\n      { value: 'Europe/Skopje', label: 'Skopje' },\n      { value: 'Europe/Sofia', label: 'Sofia' },\n      { value: 'Europe/Stockholm', label: 'Stockholm' },\n      { value: 'Europe/Tallinn', label: 'Tallinn' },\n      { value: 'Europe/Tirane', label: 'Tirane' },\n      { value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' },\n      { value: 'Europe/Uzhgorod', label: 'Uzhgorod' },\n      { value: 'Europe/Vaduz', label: 'Vaduz' },\n      { value: 'Europe/Vatican', label: 'Vatican' },\n      { value: 'Europe/Vienna', label: 'Vienna' },\n      { value: 'Europe/Vilnius', label: 'Vilnius' },\n      { value: 'Europe/Volgograd', label: 'Volgograd' },\n      { value: 'Europe/Warsaw', label: 'Warsaw' },\n      { value: 'Europe/Zagreb', label: 'Zagreb' },\n      { value: 'Europe/Zaporozhye', label: 'Zaporozhye' },\n      { value: 'Europe/Zurich', label: 'Zurich' },\n    ],\n  },\n  {\n    label: 'Indian',\n    options: [\n      { value: 'Indian/Antananarivo', label: 'Antananarivo' },\n      { value: 'Indian/Chagos', label: 'Chagos' },\n      { value: 'Indian/Christmas', label: 'Christmas' },\n      { value: 'Indian/Cocos', label: 'Cocos' },\n      { value: 'Indian/Comoro', label: 'Comoro' },\n      { value: 'Indian/Kerguelen', label: 'Kerguelen' },\n      { value: 'Indian/Mahe', label: 'Mahe' },\n      { value: 'Indian/Maldives', label: 'Maldives' },\n      { value: 'Indian/Mauritius', label: 'Mauritius' },\n      { value: 'Indian/Mayotte', label: 'Mayotte' },\n      { value: 'Indian/Reunion', label: 'Reunion' },\n    ],\n  },\n  {\n    label: 'Pacific',\n    options: [\n      { value: 'Pacific/Apia', label: 'Apia' },\n      { value: 'Pacific/Auckland', label: 'Auckland' },\n      { value: 'Pacific/Bougainville', label: 'Bougainville' },\n      { value: 'Pacific/Chatham', label: 'Chatham' },\n      { value: 'Pacific/Chuuk', label: 'Chuuk' },\n      { value: 'Pacific/Easter', label: 'Easter' },\n      { value: 'Pacific/Efate', label: 'Efate' },\n      { value: 'Pacific/Enderbury', label: 'Enderbury' },\n      { value: 'Pacific/Fakaofo', label: 'Fakaofo' },\n      { value: 'Pacific/Fiji', label: 'Fiji' },\n      { value: 'Pacific/Funafuti', label: 'Funafuti' },\n\n      { value: 'Pacific/Galapagos', label: 'Galapagos' },\n      { value: 'Pacific/Gambier', label: 'Gambier' },\n      { value: 'Pacific/Guadalcanal', label: 'Guadalcanal' },\n      { value: 'Pacific/Guam', label: 'Guam' },\n      { value: 'Pacific/Honolulu', label: 'Honolulu' },\n      { value: 'Pacific/Kiritimati', label: 'Kiritimati' },\n      { value: 'Pacific/Kosrae', label: 'Kosrae' },\n      { value: 'Pacific/Kwajalein', label: 'Kwajalein' },\n      { value: 'Pacific/Majuro', label: 'Majuro' },\n      { value: 'Pacific/Marquesas', label: 'Marquesas' },\n      { value: 'Pacific/Midway', label: 'Midway' },\n      { value: 'Pacific/Nauru', label: 'Nauru' },\n      { value: 'Pacific/Niue', label: 'Niue' },\n      { value: 'Pacific/Norfolk', label: 'Norfolk' },\n      { value: 'Pacific/Noumea', label: 'Noumea' },\n      { value: 'Pacific/Pago_Pago', label: 'Pago Pago' },\n      { value: 'Pacific/Palau', label: 'Palau' },\n      { value: 'Pacific/Pitcairn', label: 'Pitcairn' },\n      { value: 'Pacific/Pohnpei', label: 'Pohnpei' },\n      { value: 'Pacific/Port_Moresby', label: 'Port Moresby' },\n      { value: 'Pacific/Rarotonga', label: 'Rarotonga' },\n      { value: 'Pacific/Saipan', label: 'Saipan' },\n      { value: 'Pacific/Tahiti', label: 'Tahiti' },\n      { value: 'Pacific/Tarawa', label: 'Tarawa' },\n      { value: 'Pacific/Tongatapu', label: 'Tongatapu' },\n      { value: 'Pacific/Wake', label: 'Wake' },\n      { value: 'Pacific/Wallis', label: 'Wallis' },\n    ],\n  },\n\n  {\n    label: 'UTC',\n    options: [{ value: 'UTC', label: 'UTC' }],\n  },\n];\nexport const DEFAULT_TIMEZONE = 'UTC';\n\nexport const TIMELINE_NORMAL_ACTIVITY_TYPE = [\n  'undeleted',\n  'deleted',\n  'downvote',\n  'upvote',\n  'reopened',\n  'closed',\n  'pin',\n  'unpin',\n  'show',\n  'hide',\n];\n\nexport const SYSTEM_AVATAR_OPTIONS = [\n  {\n    label: 'System',\n    value: 'system',\n  },\n  {\n    label: 'Gravatar',\n    value: 'gravatar',\n  },\n];\n\nexport const TAG_SLUG_NAME_MAX_LENGTH = 35;\n\nexport const DEFAULT_THEME_COLOR = '#0033ff';\n\nexport const SUSPENSE_USER_TIME = [\n  {\n    label: 'hours',\n    time: '24',\n    value: '24h',\n  },\n  {\n    label: 'hours',\n    time: '48',\n    value: '48h',\n  },\n  {\n    label: 'hours',\n    time: '72',\n    value: '72h',\n  },\n  {\n    label: 'days',\n    time: '7',\n    value: '7d',\n  },\n  {\n    label: 'days',\n    time: '14',\n    value: '14d',\n  },\n  {\n    label: 'months',\n    time: '1',\n    value: '1m',\n  },\n  {\n    label: 'months',\n    time: '2',\n    value: '2m',\n  },\n  {\n    label: 'months',\n    time: '3',\n    value: '3m',\n  },\n  {\n    label: 'months',\n    time: '6',\n    value: '6m',\n  },\n  {\n    label: 'year',\n    time: '1',\n    value: '1y',\n  },\n];\n"
  },
  {
    "path": "ui/src/common/interface.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport interface FormValue<T = any> {\n  value: T;\n  isInvalid: boolean;\n  errorMsg: string;\n  [prop: string]: any;\n}\n\nexport interface FormDataType {\n  [prop: string]: FormValue;\n}\n\nexport interface FieldError {\n  error_field: string;\n  error_msg: string;\n}\n\nexport interface Paging {\n  page: number;\n  page_size?: number;\n}\n\nexport type ReportType = 'question' | 'answer' | 'comment' | 'user';\nexport type ReportAction = 'close' | 'flag' | 'review';\nexport interface ReportParams {\n  type: ReportType;\n  action: ReportAction;\n}\n\nexport interface TagBase {\n  display_name: string;\n  slug_name: string;\n  original_text?: string;\n  recommend?: boolean;\n  reserved?: boolean;\n}\n\nexport interface Tag extends TagBase {\n  main_tag_slug_name?: string;\n  parsed_text?: string;\n  tag_id?: string;\n}\n\nexport interface SynonymsTag extends Tag {\n  tag_id: string;\n  tag?: string;\n}\n\nexport interface TagInfo extends TagBase {\n  tag_id: string;\n  original_text: string;\n  parsed_text: string;\n  follow_count: number;\n  question_count: number;\n  is_follower: boolean;\n  member_actions;\n  created_at?;\n  updated_at?;\n  main_tag_slug_name?: string;\n  excerpt?;\n  status: string;\n}\nexport interface QuestionParams extends ImgCodeReq {\n  title: string;\n  url_title?: string;\n  content: string;\n  tags: Tag[];\n}\n\nexport interface QuestionWithAnswer extends QuestionParams {\n  answer_content: string;\n}\n\nexport interface ListResult<T = any> {\n  count: number;\n  list: T[];\n}\n\nexport interface AnswerParams extends ImgCodeReq {\n  content: string;\n  html: string;\n  question_id: string;\n  id: string;\n  edit_summary?: string;\n}\n\nexport interface LoginReqParams {\n  e_mail: string;\n  /** password */\n  pass: string;\n  captcha_id?: string;\n  captcha_code?: string;\n}\n\nexport interface RegisterReqParams extends LoginReqParams {\n  name: string;\n}\n\nexport interface ModifyPasswordReq {\n  old_pass: string;\n  pass: string;\n}\n\n/** User  */\nexport interface ModifyUserReq {\n  display_name: string;\n  username?: string;\n  avatar: any;\n  bio: string;\n  bio_html?: string;\n  location: string;\n  website: string;\n}\n\nenum RoleId {\n  User = 1,\n  Admin = 2,\n  Moderator = 3,\n}\n\nexport interface User {\n  username: string;\n  rank: number;\n  vote_count: number;\n  display_name: string;\n  avatar: string;\n}\n\nexport interface UserInfoBase {\n  id?: string;\n  avatar: any;\n  username: string;\n  display_name: string;\n  rank: number;\n  website: string;\n  location: string;\n  ip_info?: string;\n  status?: 'normal' | 'suspended' | 'deleted' | 'inactive';\n  /** roles */\n  role_id?: RoleId;\n}\n\nexport interface UserInfoRes extends UserInfoBase {\n  bio: string;\n  bio_html: string;\n  create_time?: string;\n  /**\n   * value = 1 active;\n   * value = 2 inactivated\n   */\n  mail_status: number;\n  language: string;\n  e_mail?: string;\n  have_password: boolean;\n  [prop: string]: any;\n}\n\nexport type UploadType = 'post' | 'avatar' | 'branding' | 'post_attachment';\nexport interface UploadReq {\n  file: FormData;\n}\n\nexport interface ImgCodeReq {\n  captcha_id?: string;\n  captcha_code?: string;\n}\n\nexport interface ImgCodeRes {\n  captcha_id: string;\n  captcha_img: string;\n  verify: boolean;\n}\n\nexport interface PasswordResetReq extends ImgCodeReq {\n  e_mail: string;\n}\n\nexport interface PasswordReplaceReq extends ImgCodeReq {\n  code: string;\n  pass: string;\n}\n\nexport interface CaptchaReq extends ImgCodeReq {\n  verify: ImgCodeRes['verify'];\n}\n\nexport type CaptchaKey =\n  | 'email'\n  | 'password'\n  | 'edit_userinfo'\n  | 'question'\n  | 'answer'\n  | 'comment'\n  | 'edit'\n  | 'invitation_answer'\n  | 'search'\n  | 'report'\n  | 'delete'\n  | 'vote';\n\nexport interface SetNoticeReq {\n  notice_switch: boolean;\n}\n\nexport interface NotificationBadgeAward {\n  notification_id: string;\n  badge_id: string;\n  name: string;\n  icon: string;\n  level: number;\n}\nexport interface NotificationStatus {\n  inbox: number;\n  achievement: number;\n  revision: number;\n  can_revision: boolean;\n  badge_award: NotificationBadgeAward | null;\n}\n\nexport interface QuestionDetailRes {\n  id: string;\n  title: string;\n  content: string;\n  html: string;\n  tags: any[];\n  view_count: number;\n  unique_view_count?: number;\n  answer_count: number;\n  favorites_count: number;\n  follow_counts: 0;\n  accepted_answer_id: string;\n  last_answer_id: string;\n  create_time: string;\n  update_time: string;\n  user_info: UserInfoBase;\n  answered: boolean;\n  collected: boolean;\n  answer_ids: string[];\n\n  [prop: string]: any;\n}\n\nexport interface AnswersReq extends Paging {\n  order?: 'default' | 'updated' | 'created';\n  question_id: string;\n}\n\nexport interface AnswerItem {\n  id: string;\n  question_id: string;\n  content: string;\n  html: string;\n  create_time: string;\n  update_time: string;\n  user_info: UserInfoBase;\n  [prop: string]: any;\n}\n\nexport interface PostAnswerReq extends ImgCodeReq {\n  content: string;\n  html?: string;\n  question_id: string;\n}\n\nexport interface PageUser {\n  id?;\n  displayName;\n  userName?;\n  avatar_url?;\n}\n\nexport interface LangsType {\n  label: string;\n  value: string;\n}\n\n/**\n * @description interface for Question\n */\nexport type QuestionOrderBy =\n  | 'recommend'\n  | 'newest'\n  | 'active'\n  | 'hot'\n  | 'score'\n  | 'unanswered'\n  | 'frequent';\n\nexport interface QueryQuestionsReq extends Paging {\n  order: QuestionOrderBy;\n  tag?: string;\n  in_days?: number;\n}\n\nexport type AdminQuestionStatus =\n  | 'available'\n  | 'pending'\n  | 'closed'\n  | 'deleted';\n\nexport type AdminContentsFilterBy = 'normal' | 'pending' | 'closed' | 'deleted';\n\nexport interface AdminContentsReq extends Paging {\n  status: AdminContentsFilterBy;\n  query?: string;\n}\n\n/**\n * @description interface for Answer\n */\nexport type AdminAnswerStatus = 'available' | 'deleted';\n\n/**\n * @description interface for Users\n */\nexport type UserFilterBy =\n  | 'normal'\n  | 'staff'\n  | 'inactive'\n  | 'suspended'\n  | 'deleted';\n\nexport type BadgeFilterBy = 'all' | 'active' | 'inactive';\n\nexport type InstalledPluginsFilterBy =\n  | 'all'\n  | 'active'\n  | 'inactive'\n  | 'outdated';\n/**\n * @description interface for Flags\n */\nexport type FlagStatus = 'pending' | 'completed';\nexport type FlagType = 'all' | 'question' | 'answer' | 'comment';\nexport interface AdminFlagsReq extends Paging {\n  status: FlagStatus;\n  object_type: FlagType;\n}\n\n/**\n * @description interface for Admin Settings\n */\nexport interface AdminSettingsGeneral {\n  name: string;\n  short_description: string;\n  description: string;\n  site_url: string;\n  contact_email: string;\n  permalink?: number;\n}\n\nexport interface HelmetBase {\n  pageTitle?: string;\n  description?: string;\n  keywords?: string;\n}\n\nexport interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {\n  title?: string;\n  subtitle?: string;\n}\n\nexport interface AdminSettingsInterface {\n  language: string;\n  time_zone?: string;\n}\n\nexport interface AdminSettingsSmtp {\n  encryption: string;\n  from_email: string;\n  from_name: string;\n  smtp_authentication: boolean;\n  smtp_host: string;\n  smtp_password?: string;\n  smtp_port: number;\n  smtp_username?: string;\n  test_email_recipient?: string;\n}\n\nexport interface AdminSettingsUsers {\n  allow_update_avatar: boolean;\n  allow_update_bio: boolean;\n  allow_update_display_name: boolean;\n  allow_update_location: boolean;\n  allow_update_username: boolean;\n  allow_update_website: boolean;\n  default_avatar: string;\n  gravatar_base_url: string;\n}\n\nexport interface AdminSettingsSecurity {\n  external_content_display: string;\n  check_update: boolean;\n  login_required: boolean;\n}\n\nexport interface SiteSettings {\n  branding: AdminSettingBranding;\n  general: AdminSettingsGeneral;\n  interface: AdminSettingsInterface;\n  login: AdminSettingsLogin;\n  custom_css_html: AdminSettingsCustom;\n  theme: AdminSettingsTheme;\n  site_seo: AdminSettingsSeo;\n  site_users: AdminSettingsUsers;\n  site_advanced: AdminSettingsWrite;\n  site_questions: AdminQuestionSetting;\n  site_tags: AdminTagsSetting;\n  version: string;\n  revision: string;\n  site_security: AdminSettingsSecurity;\n  ai_enabled: boolean;\n}\n\nexport interface AdminSettingBranding {\n  logo: string;\n  square_icon: string;\n  mobile_logo?: string;\n  favicon?: string;\n}\n\nexport interface AdminSettingsLegal {\n  privacy_policy_original_text?: string;\n  privacy_policy_parsed_text?: string;\n  terms_of_service_original_text?: string;\n  terms_of_service_parsed_text?: string;\n}\n\nexport interface AdminSettingsWrite {\n  max_image_size?: number;\n  max_attachment_size?: number;\n  max_image_megapixel?: number;\n  authorized_image_extensions?: string[];\n  authorized_attachment_extensions?: string[];\n}\n\nexport interface AdminSettingsSeo {\n  robots: string;\n  /**\n   * 0: not set\n   * 1：with title\n   * 2: no title\n   */\n  permalink: number;\n}\n\nexport type themeConfig = {\n  navbar_style: string;\n  primary_color: string;\n  [k: string]: string | number;\n};\nexport interface AdminSettingsTheme {\n  theme: string;\n  color_scheme: string;\n  layout: string;\n  theme_options?: { label: string; value: string }[];\n  theme_config: Record<string, themeConfig>;\n}\n\nexport interface AdminSettingsCustom {\n  custom_css: string;\n  custom_head: string;\n  custom_header: string;\n  custom_footer: string;\n  custom_sidebar: string;\n}\n\nexport interface AdminSettingsLogin {\n  allow_new_registrations: boolean;\n  allow_email_registrations: boolean;\n  allow_email_domains: string[];\n  allow_password_login: boolean;\n}\n\n/**\n * @description interface for Activity\n */\nexport interface FollowParams {\n  is_cancel: boolean;\n  object_id: string;\n}\n\n/**\n * @description search request params\n */\nexport interface SearchParams extends ImgCodeReq {\n  q: string;\n  order: string;\n  page: number;\n  size?: number;\n}\n\n/**\n * @description search response data\n */\nexport interface SearchResItem {\n  object_type: string;\n  object: {\n    url_title?: string;\n    id: string;\n    question_id?: string;\n    title: string;\n    excerpt: string;\n    created_at: number;\n    user_info: UserInfoBase;\n    vote_count: number;\n    answer_count: number;\n    accepted: boolean;\n    tags: TagBase[];\n    status?: string;\n  };\n}\nexport interface SearchRes extends ListResult<SearchResItem> {\n  extra: any;\n}\n\nexport interface AdminDashboard {\n  info: {\n    question_count: number;\n    resolved_count: number;\n    resolved_rate: string;\n    unanswered_count: number;\n    unanswered_rate: string;\n    answer_count: number;\n    comment_count: number;\n    vote_count: number;\n    user_count: number;\n    report_count: number;\n    uploading_files: boolean;\n    smtp: 'enabled' | 'disabled' | 'not_configured';\n    time_zone: string;\n    occupying_storage_space: string;\n    app_start_time: number;\n    https: boolean;\n    login_required: boolean;\n    go_version: string;\n    database_version: string;\n    database_size: string;\n    version_info: {\n      remote_version: string;\n      version: string;\n    };\n  };\n}\n\nexport interface TimelineReq {\n  show_vote: boolean;\n  object_id: string;\n}\n\nexport interface TimelineItem {\n  activity_id: number;\n  revision_id: number;\n  created_at: number;\n  activity_type: string;\n  comment: string;\n  object_id: string;\n  object_type: string;\n  cancelled: boolean;\n  cancelled_at: any;\n  user_info: UserInfoBase;\n}\n\nexport interface TimelineObject {\n  title: string;\n  url_title?: string;\n  object_type: string;\n  question_id: string;\n  answer_id: string;\n  main_tag_slug_name?: string;\n  display_name?: string;\n}\n\nexport interface TimelineRes {\n  object_info: TimelineObject;\n  timeline: TimelineItem[];\n}\n\nexport interface SuggestReviewItem {\n  type: 'question' | 'answer' | 'tag';\n  info: {\n    url_title?: string;\n    object_id: string;\n    title: string;\n    content: string;\n    html: string;\n    tags: Tag[];\n  };\n  unreviewed_info: {\n    id: string;\n    use_id: string;\n    object_id: string;\n    title: string;\n    status: 0 | 1;\n    create_at: number;\n    user_info: UserInfoBase;\n    reason: string;\n    content: Tag | QuestionDetailRes | AnswerItem;\n  };\n}\nexport interface SuggestReviewResp {\n  count: number;\n  list: SuggestReviewItem[];\n}\n\nexport interface ReasonItem {\n  content_type: string;\n  description: string;\n  name: string;\n  placeholder: string;\n  reason_type: number;\n}\n\nexport interface BaseReviewItem {\n  object_type: 'question' | 'answer' | 'comment' | 'user';\n  object_id: string;\n  object_show_status: number;\n  object_status: number;\n  tags: Tag[];\n  title: string;\n  original_text: string;\n  author_user_info: UserInfoBase;\n  created_at: number;\n  submit_at: number;\n  comment_id: string;\n  question_id: string;\n  answer_id: string;\n  answer_count: number;\n  answer_accepted?: boolean;\n  flag_id: string;\n  url_title: string;\n  parsed_text: string;\n}\n\nexport interface FlagReviewItem extends BaseReviewItem {\n  reason: ReasonItem;\n  reason_content: string;\n  submitter_user: UserInfoBase;\n}\n\nexport interface FlagReviewResp {\n  count: number;\n  list: FlagReviewItem[];\n}\n\nexport interface QueuedReviewItem extends BaseReviewItem {\n  review_id: number;\n  reason: string;\n  submitter_display_name: string;\n}\n\nexport interface QueuedReviewResp {\n  count: number;\n  list: QueuedReviewItem[];\n}\n\nexport interface UserRoleItem {\n  id: number;\n  name: string;\n  description: string;\n}\nexport interface MemberActionItem {\n  action: string;\n  name: string;\n  type: string;\n}\n\nexport interface QuestionOperationReq {\n  id: string;\n  operation: 'pin' | 'unpin' | 'hide' | 'show';\n}\n\nexport interface OauthBindEmailReq {\n  binding_key: string;\n  email: string;\n  must: boolean;\n}\n\nexport interface UserOauthConnectorItem {\n  icon: string;\n  name: string;\n  link: string;\n  binding: boolean;\n  external_id: string;\n}\n\nexport interface NotificationConfigItem {\n  enable: boolean;\n  key: string;\n}\nexport interface NotificationConfig {\n  all_new_question: NotificationConfigItem;\n  all_new_question_for_following_tags: NotificationConfigItem;\n  inbox: NotificationConfigItem;\n}\n\nexport interface ActivatedPlugin {\n  slug_name: string;\n  enabled: boolean;\n}\n\nexport interface UserPluginsConfigRes {\n  name: string;\n  slug_name: string;\n}\n\nexport interface ReviewTypeItem {\n  label: string;\n  name: string;\n  todo_amount: number;\n}\n\nexport interface PutFlagReviewParams {\n  operation_type:\n    | 'edit_post'\n    | 'close_post'\n    | 'delete_post'\n    | 'unlist_post'\n    | 'ignore_report';\n  flag_id: string;\n  close_msg?: string;\n  close_type?: number;\n  title?: string;\n  content?: string;\n  tags?: Tag[];\n  // mention_username_list?: any;\n  captcha_code?: any;\n  captcha_id?: any;\n}\n\n/**\n * @description response for reaction\n */\nexport interface ReactionItems {\n  reaction_summary: ReactionItem[];\n}\n\nexport interface ReactionItem {\n  emoji: string;\n  count: number;\n  tooltip: string;\n  is_active: boolean;\n}\n\nexport interface BadgeListItem {\n  id: string;\n  name: string;\n  icon: string;\n  award_count: number;\n  earned: boolean;\n  /** 1: bronze 2: silver 3:gold */\n  level: number;\n  earned_count?: number;\n}\n\nexport interface BadgeListGroupItem {\n  badges: BadgeListItem[];\n  group_name: string;\n}\n\nexport interface BadgeInfo extends BadgeListItem {\n  description: string;\n  earned_count: number;\n  is_single: boolean;\n}\n\nexport interface AdminBadgeListItem extends BadgeListItem {\n  group_name: string;\n  status: string;\n  description: string;\n}\n\nexport interface BadgeDetailListReq {\n  page: number;\n  page_size: number;\n  badge_id: string;\n  username?: string | null;\n}\nexport interface BadgeDetailListItem {\n  created_at: number;\n  author_user_info: UserInfoBase;\n  object_type: string;\n  object_id: string;\n  url_title: string;\n  question_id: string;\n  answer_id: string;\n  comment_id: string;\n}\n\nexport interface BadgeDetailListRes {\n  count: number;\n  list: BadgeDetailListItem[];\n}\n\nexport interface AdminApiKeysItem {\n  access_key: string;\n  created_at: number;\n  description: string;\n  id: number;\n  last_used_at: number;\n  scope: string;\n}\n\nexport interface AddOrEditApiKeyParams {\n  description: string;\n  scope?: string;\n  id?: number;\n}\n\nexport interface AiConfig {\n  enabled: boolean;\n  chosen_provider: string;\n  ai_providers: Array<{\n    provider: string;\n    api_host: string;\n    api_key: string;\n    model: string;\n  }>;\n}\n\nexport interface AiProviderItem {\n  name: string;\n  display_name: string;\n  default_api_host: string;\n}\n\nexport interface ConversationListItem {\n  conversation_id: string;\n  created_at: number;\n  topic: string;\n}\n\nexport interface AdminConversationListItem {\n  id: string;\n  topic: string;\n  helpful_count: number;\n  unhelpful_count: number;\n  created_at: number;\n  user_info: UserInfoBase;\n}\n\nexport interface ConversationDetailItem {\n  chat_completion_id: string;\n  content: string;\n  role: string;\n  helpful: number;\n  unhelpful: number;\n  created_at: number;\n}\n\nexport interface ConversationDetail {\n  conversation_id: string;\n  created_at: number;\n  records: ConversationDetailItem[];\n  topic: string;\n  updated_at: number;\n}\n\nexport interface VoteConversationParams {\n  cancel: boolean;\n  chat_completion_id: string;\n  vote_type: 'helpful' | 'unhelpful';\n}\n\nexport interface AdminQuestionSetting {\n  min_tags: number;\n  min_content: number;\n  restrict_answer: boolean;\n}\n\nexport interface AdminTagsSetting {\n  recommend_tags: Tag[];\n  required_tag: boolean;\n  reserved_tags: Tag[];\n}\n"
  },
  {
    "path": "ui/src/common/pattern.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst pattern = {\n  email:\n    /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]+\\.)+[a-zA-Z\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]{2,}))$/,\n  search:\n    /(\\[.*\\])|(is:answer)|(is:question)|(score:\\d*)|(user:\\S*)|(answers:\\d*)/g,\n  uaWeChat: /micromessenger/i,\n  uaWeCom: /wxwork/i,\n  uaDingTalk: /dingtalk/i,\n};\n\nexport default pattern;\n"
  },
  {
    "path": "ui/src/common/sideNavLayout.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.main-mx-with {\n  width: 100%;\n  max-width: 1072px;\n}\n\n.answer-container {\n  min-height: calc(100vh - 53px - 62px);\n}\n\n.page-right-side {\n  flex: none;\n  width: 300px;\n  box-sizing: content-box;\n}\n\n// lg\n@media screen and (max-width: 1199.9px) {\n  .main-mx-with {\n    padding-left: 12px;\n    padding-right: 12px;\n  }\n\n  .page-main {\n    max-width: 100%;\n  }\n  .page-right-side {\n    width: 100%;\n    box-sizing: border-box;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/AccordionNav/index.css",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.collapse-indicator {\n  transition: all 0.2s ease;\n}\n\n.expanding .collapse-indicator {\n  transform: rotate(90deg);\n}\n\n#answerAccordion {\n  max-width: 208px;\n  .nav-link {\n    color: var(--an-side-nav-link);\n  }\n  .nav-link:focus-visible {\n    box-shadow: none;\n  }\n  .nav-link:hover {\n    color: var(--an-side-nav-link-hover-color);\n    background-color: var(--bs-gray-100);\n  }\n  .nav-link.active {\n    color: var(--an-side-nav-link-hover-color);\n    background-color: var(--bs-gray-200);\n  }\n}\n"
  },
  {
    "path": "ui/src/components/AccordionNav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, useEffect, useState } from 'react';\nimport { Accordion, Nav } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useNavigate, useMatch, NavLink } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { floppyNavigation } from '@/utils';\nimport { Icon } from '@/components';\nimport './index.css';\n\nexport interface MenuItem {\n  name: string;\n  path?: string;\n  pathPrefix?: string;\n  icon?: string;\n  displayName?: string;\n  badgeContent?: string | number;\n  children?: MenuItem[];\n}\n\nfunction MenuNode({\n  menu,\n  callback,\n  activeKey,\n  expanding = false,\n  path = '/',\n}: {\n  menu: MenuItem;\n  callback: (evt: any, menu: MenuItem, href: string, isLeaf: boolean) => void;\n  activeKey: string;\n  expanding?: boolean;\n  path?: string;\n}) {\n  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });\n  const isLeaf = !menu.children || menu.children.length === 0;\n  const href = isLeaf ? `${path}${menu.path || ''}` : '#';\n\n  return (\n    <Nav.Item key={menu.path} className=\"w-100\">\n      {isLeaf ? (\n        <Nav.Link\n          eventKey={menu.path}\n          as={NavLink}\n          to={href}\n          onClick={(evt) => {\n            callback(evt, menu, href, isLeaf);\n          }}\n          className={classNames(\n            'text-nowrap d-flex flex-nowrap align-items-center w-100',\n            {\n              expanding,\n              active:\n                activeKey === menu.path ||\n                (menu.path && activeKey.startsWith(`${menu.path}/`)) ||\n                // if pathPrefix is set, activate when activeKey starts with the pathPrefix\n                (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),\n            },\n          )}>\n          {menu?.icon && <Icon name={menu.icon} className=\"me-2\" />}\n\n          <span className=\"me-auto text-truncate\">\n            {menu.displayName ? menu.displayName : t(menu.name)}\n          </span>\n          {menu.badgeContent ? (\n            <span className=\"badge text-bg-dark\">{menu.badgeContent}</span>\n          ) : null}\n          {!isLeaf && (\n            <Icon className=\"collapse-indicator\" name=\"chevron-right\" />\n          )}\n        </Nav.Link>\n      ) : (\n        <Nav.Link\n          eventKey={menu.path}\n          as=\"button\"\n          href={href}\n          onClick={(evt) => {\n            callback(evt, menu, href, isLeaf);\n          }}\n          className={classNames(\n            'text-nowrap d-flex flex-nowrap align-items-center w-100',\n            {\n              expanding,\n              active:\n                activeKey === menu.path ||\n                (menu.path && activeKey.startsWith(`${menu.path}/`)) ||\n                (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),\n            },\n          )}>\n          {menu?.icon && <Icon name={menu.icon} className=\"me-2\" />}\n          <span className=\"me-auto text-truncate\">\n            {menu.displayName ? menu.displayName : t(menu.name)}\n          </span>\n          {menu.badgeContent ? (\n            <span className=\"badge text-bg-dark\">{menu.badgeContent}</span>\n          ) : null}\n          {!isLeaf && (\n            <Icon className=\"collapse-indicator\" name=\"chevron-right\" />\n          )}\n        </Nav.Link>\n      )}\n\n      {menu.children && menu.children.length > 0 ? (\n        <Accordion.Collapse eventKey={menu.path || menu.name} className=\"ms-4\">\n          <>\n            {menu.children.map((leaf) => {\n              return (\n                <MenuNode\n                  menu={leaf}\n                  callback={callback}\n                  activeKey={activeKey}\n                  path={path}\n                  key={leaf.path || leaf.name}\n                />\n              );\n            })}\n          </>\n        </Accordion.Collapse>\n      ) : null}\n    </Nav.Item>\n  );\n}\n\ninterface AccordionProps {\n  menus: MenuItem[];\n  path?: string;\n}\nconst AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {\n  const navigate = useNavigate();\n  const pathMatch = useMatch(`${path}*`);\n  // auto set menu fields\n  menus.forEach((m) => {\n    if (!m.path) {\n      m.path = m.name;\n    }\n    if (!Array.isArray(m.children)) {\n      m.children = [];\n    }\n    m.children.forEach((sm) => {\n      if (!sm.path) {\n        sm.path = sm.name;\n      }\n      if (!Array.isArray(sm.children)) {\n        sm.children = [];\n      }\n    });\n  });\n\n  const splat = pathMatch && pathMatch.params['*'];\n  let activeKey: string = menus[0]?.path || menus[0]?.name || '';\n\n  if (splat) {\n    activeKey = splat;\n  }\n\n  const getOpenKey = () => {\n    let openKey = '';\n    menus.forEach((li) => {\n      if (li.children && li.children.length > 0) {\n        const matchedChild = li.children.find((el) => {\n          // exact match or path prefix match\n          return (\n            el.path === activeKey ||\n            (el.path && activeKey.startsWith(`${el.path}/`)) ||\n            // if pathPrefix is set, activate when activeKey starts with the pathPrefix\n            (el.pathPrefix && activeKey.startsWith(el.pathPrefix))\n          );\n        });\n        if (matchedChild) {\n          openKey = li.path || li.name || '';\n        }\n      }\n    });\n    return openKey;\n  };\n\n  const [openKey, setOpenKey] = useState(getOpenKey());\n  const menuClick = (evt, menu, href, isLeaf) => {\n    evt.stopPropagation();\n    if (isLeaf) {\n      if (floppyNavigation.shouldProcessLinkClick(evt)) {\n        evt.preventDefault();\n        navigate(href);\n      }\n    } else {\n      setOpenKey(openKey === menu.path ? '' : menu.path);\n    }\n  };\n  useEffect(() => {\n    setOpenKey(getOpenKey());\n  }, [activeKey, menus]);\n  return (\n    <Accordion activeKey={openKey} flush id=\"answerAccordion\">\n      <Nav variant=\"pills\" className=\"flex-column\" activeKey={activeKey}>\n        {menus.map((li) => {\n          return (\n            <MenuNode\n              menu={li}\n              path={path}\n              callback={menuClick}\n              activeKey={activeKey}\n              expanding={openKey === (li.path || li.name)}\n              key={li.path || li.name}\n            />\n          );\n        })}\n      </Nav>\n    </Accordion>\n  );\n};\n\nexport default AccordionNav;\n"
  },
  {
    "path": "ui/src/components/Actions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState, useEffect } from 'react';\nimport { Button, ButtonGroup } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { Icon } from '@/components';\nimport { loggedUserInfoStore } from '@/stores';\nimport { useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { bookmark, postVote } from '@/services';\nimport * as Types from '@/common/interface';\n\ninterface Props {\n  className?: string;\n  source: 'question' | 'answer';\n  data: {\n    id: string;\n    votesCount: number;\n    isLike: boolean;\n    isHate: boolean;\n    hideCollect?: boolean;\n    collected: boolean;\n    collectCount: number;\n    username: string;\n  };\n}\n\nconst Index: FC<Props> = ({ className, data, source }) => {\n  const [votes, setVotes] = useState(0);\n  const [like, setLike] = useState(false);\n  const [hate, setHated] = useState(false);\n  const [bookmarkState, setBookmark] = useState({\n    state: data?.collected,\n    count: data?.collectCount,\n  });\n  const { username = '' } = loggedUserInfoStore((state) => state.user);\n  const toast = useToast();\n  const { t } = useTranslation();\n  const vCaptcha = useCaptchaPlugin('vote');\n\n  useEffect(() => {\n    if (data) {\n      setVotes(data.votesCount);\n      setLike(data.isLike);\n      setHated(data.isHate);\n      setBookmark({\n        state: data?.collected,\n        count: data?.collectCount,\n      });\n    }\n  }, []);\n\n  const submitVote = (type) => {\n    const isCancel = (type === 'up' && like) || (type === 'down' && hate);\n    const imgCode: Types.ImgCodeReq = {\n      captcha_id: undefined,\n      captcha_code: undefined,\n    };\n    vCaptcha?.resolveCaptchaReq?.(imgCode);\n\n    postVote(\n      {\n        object_id: data?.id,\n        is_cancel: isCancel,\n        ...imgCode,\n      },\n      type,\n    )\n      .then(async (res) => {\n        await vCaptcha?.close();\n        setVotes(res.votes);\n        setLike(res.vote_status === 'vote_up');\n        setHated(res.vote_status === 'vote_down');\n      })\n      .catch((err) => {\n        if (err?.isError) {\n          vCaptcha?.handleCaptchaError(err.list);\n        }\n        const errMsg = err?.value;\n        if (errMsg) {\n          toast.onShow({\n            msg: errMsg,\n            variant: 'danger',\n          });\n        }\n      });\n  };\n\n  const handleVote = (type: 'up' | 'down') => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n\n    if (data.username === username) {\n      toast.onShow({\n        msg: t('cannot_vote_for_self'),\n        variant: 'danger',\n      });\n      return;\n    }\n\n    if (!vCaptcha) {\n      submitVote(type);\n      return;\n    }\n\n    vCaptcha.check(() => {\n      submitVote(type);\n    });\n  };\n\n  const handleBookmark = () => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    bookmark({\n      group_id: '0',\n      object_id: data?.id,\n      bookmark: !bookmarkState.state,\n    }).then((res) => {\n      setBookmark({\n        state: !bookmarkState.state,\n        count: res.object_collection_count,\n      });\n    });\n  };\n\n  return (\n    <div className={classNames(className)}>\n      <ButtonGroup>\n        <Button\n          title={\n            source === 'question'\n              ? t('question_detail.question_useful')\n              : t('question_detail.answer_useful')\n          }\n          variant=\"outline-secondary\"\n          active={like}\n          onClick={() => handleVote('up')}>\n          <Icon name=\"hand-thumbs-up-fill\" />\n        </Button>\n        <Button variant=\"outline-secondary\" className=\"opacity-100\" disabled>\n          {votes}\n        </Button>\n        <Button\n          title={\n            source === 'question'\n              ? t('question_detail.question_un_useful')\n              : t('question_detail.answer_un_useful')\n          }\n          variant=\"outline-secondary\"\n          active={hate}\n          onClick={() => handleVote('down')}>\n          <Icon name=\"hand-thumbs-down-fill\" />\n        </Button>\n      </ButtonGroup>\n      {!data?.hideCollect && (\n        <Button\n          variant=\"outline-secondary ms-3\"\n          title={t('question_detail.question_bookmark')}\n          active={bookmarkState.state}\n          onClick={handleBookmark}>\n          <Icon name=\"bookmark-fill\" />\n          <span style={{ paddingLeft: '10px' }}>{bookmarkState.count}</span>\n        </Button>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/AdminSideNav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect } from 'react';\nimport { NavLink } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport cloneDeep from 'lodash/cloneDeep';\n\nimport { AccordionNav, Icon } from '@/components';\nimport type { MenuItem } from '@/components/AccordionNav';\nimport { ADMIN_NAV_MENUS } from '@/common/constants';\nimport { useQueryPlugins } from '@/services';\nimport { interfaceStore } from '@/stores';\n\nconst AdminSideNav = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'btns' });\n  const interfaceLang = interfaceStore((_) => _.interface.language);\n  const { data: configurablePlugins, mutate: updateConfigurablePlugins } =\n    useQueryPlugins({\n      status: 'active',\n      have_config: true,\n    });\n\n  const menus = cloneDeep(ADMIN_NAV_MENUS) as MenuItem[];\n  if (configurablePlugins && configurablePlugins.length > 0) {\n    menus.forEach((item) => {\n      if (item.name === 'plugins' && item.children) {\n        item.children = [\n          ...item.children,\n          ...configurablePlugins.map(\n            (plugin): MenuItem => ({\n              name: plugin.slug_name,\n              displayName: plugin.name,\n            }),\n          ),\n        ];\n      }\n    });\n  }\n\n  const observePlugins = (evt) => {\n    if (evt.data.msgType === 'refreshConfigurablePlugins') {\n      updateConfigurablePlugins();\n    }\n  };\n  useEffect(() => {\n    window.addEventListener('message', observePlugins);\n    return () => {\n      window.removeEventListener('message', observePlugins);\n    };\n  }, []);\n  useEffect(() => {\n    updateConfigurablePlugins();\n  }, [interfaceLang]);\n\n  return (\n    <div id=\"adminSideNav\">\n      <NavLink to=\"/\" className=\"pb-3 d-inline-block link-secondary\">\n        <Icon name=\"arrow-left\" className=\"me-2\" />\n        <span>{t('back_sites')}</span>\n      </NavLink>\n      <AccordionNav menus={menus} path=\"/admin/\" />\n    </div>\n  );\n};\n\nexport default AdminSideNav;\n"
  },
  {
    "path": "ui/src/components/Avatar/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\n\nimport classNames from 'classnames';\n\nimport DefaultAvatar from '@/assets/images/default-avatar.svg';\n\ninterface IProps {\n  /** avatar url */\n  avatar: string | { type: string; gravatar: string; custom: string };\n  /** size 48 96 128 256 */\n  size: string;\n  searchStr?: string;\n  className?: string;\n  alt: string;\n}\n\nconst Index: FC<IProps> = ({\n  avatar,\n  size,\n  className,\n  searchStr = '',\n  alt,\n}) => {\n  let url = '';\n  if (typeof avatar === 'string') {\n    if (avatar.length > 1) {\n      url = `${avatar}?${searchStr}${\n        avatar?.includes('gravatar') ? '&d=identicon' : ''\n      }`;\n    }\n  } else if (avatar?.type === 'gravatar' && avatar.gravatar) {\n    url = `${avatar.gravatar}?${searchStr}&d=identicon`;\n  } else if (avatar?.type === 'custom' && avatar.custom) {\n    url = `${avatar.custom}?${searchStr}`;\n  }\n\n  const roundedCls =\n    className && className.indexOf('rounded') !== -1 ? '' : 'rounded-circle';\n\n  return (\n    <img\n      src={url || DefaultAvatar}\n      width={size}\n      height={size}\n      className={classNames(roundedCls, className)}\n      alt={alt}\n    />\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/BaseUserCard/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { Avatar } from '@/components';\nimport { formatCount } from '@/utils';\n\ninterface Props {\n  data: any;\n  showAvatar?: boolean;\n  avatarSize?: string;\n  showReputation?: boolean;\n  avatarSearchStr?: string;\n  className?: string;\n  avatarClass?: string;\n  nameMaxWidth?: string;\n}\n\nconst Index: FC<Props> = ({\n  data,\n  showAvatar = true,\n  avatarClass = '',\n  avatarSize = '24px',\n  className = 'small',\n  avatarSearchStr = 's=48',\n  showReputation = true,\n  nameMaxWidth = '300px',\n}) => {\n  return (\n    <div className={`d-flex align-items-center  text-secondary ${className}`}>\n      {data?.status !== 'deleted' ? (\n        <Link\n          to={`/users/${data?.username}`}\n          onClick={(e) => {\n            e.stopPropagation();\n          }}\n          className=\"d-flex align-items-center\">\n          {showAvatar && (\n            <Avatar\n              avatar={data?.avatar}\n              size={avatarSize}\n              className={`me-1 ${avatarClass}`}\n              searchStr={avatarSearchStr}\n              alt={data?.display_name}\n            />\n          )}\n          <span\n            className=\"me-1 name-ellipsis\"\n            style={{ maxWidth: nameMaxWidth }}>\n            {data?.display_name}\n          </span>\n        </Link>\n      ) : (\n        <>\n          {showAvatar && (\n            <Avatar\n              avatar={data?.avatar}\n              size={avatarSize}\n              className={`me-1 ${avatarClass}`}\n              searchStr={avatarSearchStr}\n              alt={data?.display_name}\n            />\n          )}\n          <span className=\"me-1 name-ellipsis\">{data?.display_name}</span>\n        </>\n      )}\n\n      {showReputation && (\n        <span className=\"fw-bold\" title=\"Reputation\">\n          {formatCount(data?.rank)}\n        </span>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/BrandUpload/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { ButtonGroup, Button } from 'react-bootstrap';\n\nimport classNames from 'classnames';\n\nimport { Icon, UploadImg } from '@/components';\nimport { UploadType } from '@/common/interface';\n\ninterface Props {\n  type: UploadType;\n  value: string;\n  onChange: (value: string) => void;\n  acceptType?: string;\n  readOnly?: boolean;\n  imgClassNames?: classNames.Argument;\n}\n\nconst Index: FC<Props> = ({\n  type = 'post',\n  value,\n  onChange,\n  acceptType,\n  readOnly = false,\n  imgClassNames = '',\n}) => {\n  const onUpload = (imgPath: string) => {\n    onChange(imgPath);\n  };\n\n  const onRemove = () => {\n    onChange('');\n  };\n  return (\n    <div className=\"d-flex\">\n      <div className=\"bg-gray-300 upload-img-wrap me-2 d-flex align-items-center justify-content-center\">\n        <img\n          className={classNames(imgClassNames)}\n          src={value}\n          alt=\"\"\n          style={{ maxWidth: '100%', maxHeight: '100%' }}\n        />\n      </div>\n      <ButtonGroup vertical className=\"fit-content\">\n        <UploadImg\n          type={type}\n          uploadCallback={onUpload}\n          className=\"mb-0\"\n          disabled={readOnly}\n          acceptType={acceptType}>\n          <Icon name=\"cloud-upload\" />\n        </UploadImg>\n\n        <Button\n          disabled={readOnly}\n          variant=\"outline-secondary\"\n          onClick={onRemove}>\n          <Icon name=\"trash\" />\n        </Button>\n      </ButtonGroup>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/BubbleAi/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState, useRef } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { marked } from 'marked';\nimport copy from 'copy-to-clipboard';\n\nimport { voteConversation } from '@/services';\nimport { Icon, htmlRender } from '@/components';\n\ninterface IProps {\n  canType?: boolean;\n  chatId: string;\n  isLast: boolean;\n  isCompleted: boolean;\n  content: string;\n  minHeight?: number;\n  actionData: {\n    helpful: number;\n    unhelpful: number;\n  };\n}\n\nconst BubbleAi: FC<IProps> = ({\n  canType = false,\n  isLast,\n  isCompleted,\n  content,\n  chatId = '',\n  actionData,\n  minHeight = 0,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });\n  const [displayContent, setDisplayContent] = useState('');\n  const [copyText, setCopyText] = useState<string>(t('copy'));\n  const [isHelpful, setIsHelpful] = useState(false);\n  const [isUnhelpful, setIsUnhelpful] = useState(false);\n  const [canShowAction, setCanShowAction] = useState(false);\n  const typewriterRef = useRef<{\n    timer: NodeJS.Timeout | null;\n    index: number;\n    isTyping: boolean;\n  }>({\n    timer: null,\n    index: 0,\n    isTyping: false,\n  });\n  const fmtContainer = useRef<HTMLDivElement>(null);\n  // add ref for ScrollIntoView\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const handleCopy = () => {\n    const res = copy(displayContent);\n    if (res) {\n      setCopyText(t('copied', { keyPrefix: 'messages' }));\n      setTimeout(() => {\n        setCopyText(t('copy'));\n      }, 1200);\n    }\n  };\n\n  const handleVote = (voteType: 'helpful' | 'unhelpful') => {\n    const isCancel =\n      (voteType === 'helpful' && isHelpful) ||\n      (voteType === 'unhelpful' && isUnhelpful);\n    voteConversation({\n      chat_completion_id: chatId,\n      cancel: isCancel,\n      vote_type: voteType,\n    }).then(() => {\n      setIsHelpful(voteType === 'helpful' && !isCancel);\n      setIsUnhelpful(voteType === 'unhelpful' && !isCancel);\n    });\n  };\n\n  useEffect(() => {\n    if ((!canType || !isLast) && content) {\n      // 如果不是最后一个消息，直接返回，不进行打字效果\n      if (typewriterRef.current.timer) {\n        clearInterval(typewriterRef.current.timer);\n        typewriterRef.current.timer = null;\n      }\n      setDisplayContent(content);\n      setCanShowAction(true);\n      typewriterRef.current.timer = null;\n      typewriterRef.current.isTyping = false;\n      return;\n    }\n    // 当内容变化时，清理之前的计时器\n    if (typewriterRef.current.timer) {\n      clearInterval(typewriterRef.current.timer);\n      typewriterRef.current.timer = null;\n    }\n\n    // 如果内容为空，则直接返回\n    if (!content) {\n      setDisplayContent('');\n      return;\n    }\n\n    // 如果内容比当前显示的短，则重置\n    if (content.length < displayContent.length) {\n      setDisplayContent('');\n      typewriterRef.current.index = 0;\n    }\n\n    // 如果内容与显示内容相同，不需要做任何事\n    if (content === displayContent) {\n      return;\n    }\n\n    typewriterRef.current.isTyping = true;\n\n    // start typing animation\n    typewriterRef.current.timer = setInterval(() => {\n      const currentIndex = typewriterRef.current.index;\n      if (currentIndex < content.length) {\n        const remainingLength = content.length - currentIndex;\n        const baseRandomNum = Math.floor(Math.random() * 3) + 2;\n        let randomNum = Math.min(baseRandomNum, remainingLength);\n\n        // 简单的单词边界检查（可选）\n        const nextChar = content[currentIndex + randomNum];\n        const prevChar = content[currentIndex + randomNum - 1];\n\n        // 如果下一个字符是字母，当前字符也是字母，尝试调整到空格处\n        if (\n          nextChar &&\n          /[a-zA-Z]/.test(nextChar) &&\n          /[a-zA-Z]/.test(prevChar)\n        ) {\n          // 向前找1-2个字符，看看有没有空格\n          for (\n            let i = 1;\n            i <= 2 && currentIndex + randomNum - i > currentIndex;\n            i += 1\n          ) {\n            if (content[currentIndex + randomNum - i] === ' ') {\n              randomNum = randomNum - i + 1;\n              break;\n            }\n          }\n          // 向后找1-2个字符，看看有没有空格\n          for (\n            let i = 1;\n            i <= 2 && currentIndex + randomNum + i < content.length;\n            i += 1\n          ) {\n            if (content[currentIndex + randomNum + i] === ' ') {\n              randomNum = randomNum + i + 1;\n              break;\n            }\n          }\n        }\n\n        const nextIndex = currentIndex + randomNum;\n        const newContent = content.substring(0, nextIndex);\n        setDisplayContent(newContent);\n        typewriterRef.current.index = nextIndex;\n        setCanShowAction(false);\n      } else {\n        clearInterval(typewriterRef.current.timer as NodeJS.Timeout);\n        typewriterRef.current.timer = null;\n        typewriterRef.current.isTyping = false;\n        setCanShowAction(false);\n      }\n    }, 30);\n\n    // eslint-disable-next-line consistent-return\n    return () => {\n      if (typewriterRef.current.timer) {\n        clearInterval(typewriterRef.current.timer);\n        typewriterRef.current.timer = null;\n      }\n    };\n  }, [content, isCompleted]);\n\n  useEffect(() => {\n    setIsHelpful(actionData.helpful > 0);\n    setIsUnhelpful(actionData.unhelpful > 0);\n  }, [actionData]);\n\n  useEffect(() => {\n    if (fmtContainer.current && isCompleted) {\n      htmlRender(fmtContainer.current, {\n        copySuccessText: t('copied', { keyPrefix: 'messages' }),\n        copyText: t('copy', { keyPrefix: 'messages' }),\n      });\n      const links = fmtContainer.current.querySelectorAll('a');\n      links.forEach((link) => {\n        link.setAttribute('target', '_blank');\n      });\n      setCanShowAction(true);\n    }\n  }, [isCompleted, fmtContainer.current]);\n\n  return (\n    <div\n      className=\"rounded bubble-ai\"\n      ref={containerRef}\n      style={{ minHeight: `${minHeight}px`, overflowAnchor: 'none' }}>\n      <div id={chatId}>\n        <div\n          className=\"fmt text-break text-wrap\"\n          ref={fmtContainer}\n          style={{ transition: 'all 0.2s ease' }}\n          dangerouslySetInnerHTML={{ __html: marked.parse(displayContent) }}\n        />\n\n        {canShowAction && (\n          <div className=\"action\">\n            <Button\n              variant=\"link\"\n              className=\"p-0 link-secondary small me-3\"\n              onClick={handleCopy}>\n              <Icon name=\"copy\" />\n              <span className=\"ms-1\">{copyText}</span>\n            </Button>\n            <Button\n              variant=\"link\"\n              className={`p-0 small me-3 ${isHelpful ? 'link-primary active' : 'link-secondary'}`}\n              onClick={() => handleVote('helpful')}>\n              <Icon name=\"hand-thumbs-up-fill\" />\n              <span className=\"ms-1\">Helpful</span>\n            </Button>\n            <Button\n              variant=\"link\"\n              className={`p-0 small me-3 ${isUnhelpful ? 'link-primary active' : 'link-secondary'}`}\n              onClick={() => handleVote('unhelpful')}>\n              <Icon name=\"hand-thumbs-down-fill\" />\n              <span className=\"ms-1\">Unhelpful</span>\n            </Button>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default BubbleAi;\n"
  },
  {
    "path": "ui/src/components/BubbleUser/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.bubble-user-wrap {\n  scroll-margin-top: 88px;\n}\n.bubble-user {\n  background-color: var(--bs-gray-200);\n}\n"
  },
  {
    "path": "ui/src/components/BubbleUser/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport './index.scss';\n\ninterface BubbleUserProps {\n  content?: string;\n}\n\nconst BubbleUser: FC<BubbleUserProps> = ({ content }) => {\n  return (\n    <div className=\"text-end bubble-user-wrap\">\n      <div className=\"d-inline-block text-start bubble-user p-3 rounded pre-line\">\n        {content}\n      </div>\n    </div>\n  );\n};\n\nexport default BubbleUser;\n"
  },
  {
    "path": "ui/src/components/CardBadge/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.badge-card {\n  .label {\n    position: absolute;\n    top: 1rem;\n    right: 1rem;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/CardBadge/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useTranslation } from 'react-i18next';\nimport { FC } from 'react';\nimport { Card, Badge } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\n\nimport classnames from 'classnames';\n\nimport { Icon } from '@/components';\nimport * as Type from '@/common/interface';\nimport { formatCount } from '@/utils';\n\nimport './index.scss';\n\ninterface IProps {\n  data: Type.BadgeListItem;\n  showAwardedCount?: boolean;\n  urlSearchParams?: string;\n  badgePillType?: 'earned' | 'count';\n}\n\nconst Index: FC<IProps> = ({\n  data,\n  badgePillType = 'earned',\n  showAwardedCount = false,\n  urlSearchParams,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges' });\n  return (\n    <Link\n      className=\"card text-center badge-card\"\n      to={`/badges/${data.id}${urlSearchParams ? `?${urlSearchParams}` : ''}`}>\n      <Card.Body>\n        {Number(data?.earned_count) > 0 && badgePillType === 'earned' && (\n          <Badge\n            bg=\"success\"\n            style={{ position: 'absolute', top: '1rem', right: '1rem' }}>\n            {`${t('earned')}${\n              Number(data?.earned_count) > 1 ? ` ×${data.earned_count}` : ''\n            }`}\n          </Badge>\n        )}\n\n        {badgePillType === 'count' && Number(data?.earned_count) > 1 && (\n          <Badge\n            pill\n            bg=\"secondary\"\n            style={{ position: 'absolute', top: '1rem', right: '1rem' }}>\n            ×{data.earned_count}\n          </Badge>\n        )}\n        {data.icon.startsWith('http') ? (\n          <img src={data.icon} width={96} height={96} alt={data.name} />\n        ) : (\n          <Icon\n            name={data.icon}\n            size=\"96px\"\n            className={classnames(\n              'lh-1',\n              data.level === 1 && 'bronze',\n              data.level === 2 && 'silver',\n              data.level === 3 && 'gold',\n            )}\n          />\n        )}\n\n        <h6 className=\"mb-0 mt-3 text-center\">{data.name}</h6>\n        {showAwardedCount && (\n          <div className=\"small text-secondary mt-2\">\n            {t('×_awarded', { number: formatCount(data.award_count) })}\n          </div>\n        )}\n      </Card.Body>\n    </Link>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/Comment/components/ActionBar/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { Button, Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { Icon, FormatTime } from '@/components';\n\nconst ActionBar = ({\n  nickName,\n  username,\n  createdAt,\n  isVote,\n  voteCount = 0,\n  memberActions,\n  onReply,\n  onVote,\n  onAction,\n  userStatus = '',\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'comment' });\n\n  return (\n    <div className=\"d-flex justify-content-between flex-wrap small\">\n      <div className=\"d-flex align-items-center flex-wrap link-secondary\">\n        {userStatus !== 'deleted' ? (\n          <Link\n            to={`/users/${username}`}\n            className=\"name-ellipsis\"\n            style={{ maxWidth: '200px' }}>\n            {nickName}\n          </Link>\n        ) : (\n          <span>{nickName}</span>\n        )}\n        <span className=\"mx-1\">•</span>\n        <FormatTime time={createdAt} className=\"me-3 flex-shrink-0\" />\n        <Button\n          title={t('tip_vote')}\n          variant=\"link\"\n          size=\"sm\"\n          className={`flex-shrink-0 me-3 btn-no-border p-0 ${\n            isVote ? '' : 'link-secondary'\n          }`}\n          onClick={onVote}>\n          <Icon name=\"hand-thumbs-up-fill\" />\n          {voteCount > 0 && (\n            <span className=\"ms-2 link-secondary\">{voteCount}</span>\n          )}\n        </Button>\n        <Button\n          variant=\"link\"\n          size=\"sm\"\n          className=\"link-secondary m-0 p-0 btn-no-border\"\n          onClick={onReply}>\n          {t('btn_reply')}\n        </Button>\n      </div>\n      <div className=\"align-items-center control-area d-none\">\n        {memberActions.map((action, index) => {\n          return (\n            <Button\n              key={action.name}\n              variant=\"link\"\n              size=\"sm\"\n              className={classNames(\n                'link-secondary btn-no-border m-0 p-0',\n                index > 0 && 'ms-3',\n              )}\n              onClick={() => onAction(action)}>\n              {action.name}\n            </Button>\n          );\n        })}\n      </div>\n      <Dropdown className=\"d-block d-md-none\">\n        <Dropdown.Toggle\n          as=\"div\"\n          variant=\"success\"\n          className=\"no-toggle\"\n          id=\"dropdown-comment\">\n          <Icon name=\"three-dots\" className=\"text-secondary\" />\n        </Dropdown.Toggle>\n\n        <Dropdown.Menu align=\"end\">\n          {memberActions.map((action) => {\n            return (\n              <Dropdown.Item key={action.name} onClick={() => onAction(action)}>\n                {action.name}\n              </Dropdown.Item>\n            );\n          })}\n        </Dropdown.Menu>\n      </Dropdown>\n    </div>\n  );\n};\n\nexport default memo(ActionBar);\n"
  },
  {
    "path": "ui/src/components/Comment/components/Form/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect, memo } from 'react';\nimport { Button, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { TextArea, Mentions } from '@/components';\nimport { usePageUsers, usePromptWithUnload } from '@/hooks';\nimport { parseEditMentionUser } from '@/utils';\n\nconst Index = ({\n  className = '',\n  value: initialValue = '',\n  onSendReply,\n  type = '',\n  onCancel,\n  mode,\n}) => {\n  const [value, setValue] = useState('');\n  const [immData, setImmData] = useState('');\n  const pageUsers = usePageUsers();\n  const { t } = useTranslation('translation', { keyPrefix: 'comment' });\n  const [validationErrorMsg, setValidationErrorMsg] = useState('');\n\n  usePromptWithUnload({\n    when: type === 'edit' ? immData !== value : Boolean(value),\n  });\n  useEffect(() => {\n    if (!initialValue) {\n      return;\n    }\n    setImmData(initialValue);\n    setValue(initialValue);\n  }, [initialValue]);\n\n  const handleChange = (e) => {\n    setValue(e.target.value);\n  };\n  const handleSelected = (val) => {\n    setValue(val);\n  };\n  const handleSendReply = () => {\n    onSendReply(value).catch((ex) => {\n      if (ex.isError) {\n        setValidationErrorMsg(ex.msg);\n      }\n    });\n  };\n\n  return (\n    <div\n      className={classNames(\n        'd-flex align-items-start flex-column flex-md-row',\n        className,\n      )}>\n      <div className=\"w-100\">\n        <div\n          className={classNames('custom-form-control', {\n            'is-invalid': validationErrorMsg,\n          })}>\n          <Mentions\n            pageUsers={pageUsers.getUsers()}\n            onSelected={handleSelected}>\n            <TextArea\n              size=\"sm\"\n              value={type === 'edit' ? parseEditMentionUser(value) : value}\n              onChange={handleChange}\n              isInvalid={validationErrorMsg !== ''}\n            />\n          </Mentions>\n          <div className=\"form-text\">{t(`tip_${mode}`)}</div>\n        </div>\n        <Form.Control.Feedback type=\"invalid\">\n          {validationErrorMsg}\n        </Form.Control.Feedback>\n      </div>\n      {type === 'edit' ? (\n        <div className=\"d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0\">\n          <Button\n            size=\"sm\"\n            className=\"text-nowrap \"\n            onClick={() => handleSendReply()}>\n            {t('btn_save_edits')}\n          </Button>\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            className=\"text-nowrap btn-no-border ms-2 ms-md-0\"\n            onClick={onCancel}>\n            {t('btn_cancel')}\n          </Button>\n        </div>\n      ) : (\n        <Button\n          size=\"sm\"\n          className=\"text-nowrap ms-0 ms-md-2 mt-2 mt-md-0\"\n          onClick={() => handleSendReply()}>\n          {t('btn_add_comment')}\n        </Button>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Comment/components/Reply/index.tsx",
    "content": "/*\r\n * Licensed to the Apache Software Foundation (ASF) under one\r\n * or more contributor license agreements.  See the NOTICE file\r\n * distributed with this work for additional information\r\n * regarding copyright ownership.  The ASF licenses this file\r\n * to you under the Apache License, Version 2.0 (the\r\n * \"License\"); you may not use this file except in compliance\r\n * with the License.  You may obtain a copy of the License at\r\n *\r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing,\r\n * software distributed under the License is distributed on an\r\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n * KIND, either express or implied.  See the License for the\r\n * specific language governing permissions and limitations\r\n * under the License.\r\n */\r\n\r\nimport { useState, memo } from 'react';\r\nimport { Button, Form } from 'react-bootstrap';\r\nimport { useTranslation } from 'react-i18next';\r\n\r\nimport classNames from 'classnames';\r\n\r\nimport { TextArea, Mentions } from '@/components';\r\nimport { usePageUsers, usePromptWithUnload } from '@/hooks';\r\n\r\nconst Index = ({ userName, onSendReply, onCancel, mode }) => {\r\n  const [value, setValue] = useState('');\r\n  const pageUsers = usePageUsers();\r\n  const { t } = useTranslation('translation', { keyPrefix: 'comment' });\r\n  const [validationErrorMsg, setValidationErrorMsg] = useState('');\r\n\r\n  usePromptWithUnload({\r\n    when: Boolean(value),\r\n  });\r\n\r\n  const handleChange = (e) => {\r\n    setValue(e.target.value);\r\n  };\r\n  const handleSelected = (val) => {\r\n    setValue(val);\r\n  };\r\n  const handleSendReply = () => {\r\n    onSendReply(value).catch((ex) => {\r\n      if (ex.isError) {\r\n        setValidationErrorMsg(ex.msg);\r\n      }\r\n    });\r\n  };\r\n\r\n  return (\r\n    <div className=\"mb-2\">\r\n      <div className=\"small mb-2\">\r\n        {t('reply_to')} {userName}\r\n      </div>\r\n      <div className=\"d-flex mb-1 align-items-start flex-column flex-md-row\">\r\n        <div className=\"w-100\">\r\n          <div\r\n            className={classNames('custom-form-control', {\r\n              'is-invalid': validationErrorMsg,\r\n            })}>\r\n            <Mentions\r\n              pageUsers={pageUsers.getUsers()}\r\n              onSelected={handleSelected}>\r\n              <TextArea\r\n                size=\"sm\"\r\n                value={value}\r\n                onChange={handleChange}\r\n                isInvalid={validationErrorMsg !== ''}\r\n              />\r\n            </Mentions>\r\n            <div className=\"form-text\">{t(`tip_${mode}`)}</div>\r\n          </div>\r\n          <Form.Control.Feedback type=\"invalid\">\r\n            {validationErrorMsg}\r\n          </Form.Control.Feedback>\r\n        </div>\r\n        <div className=\"d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0\">\r\n          <Button\r\n            size=\"sm\"\r\n            className=\"text-nowrap\"\r\n            onClick={() => handleSendReply()}>\r\n            {t('btn_add_comment')}\r\n          </Button>\r\n          <Button\r\n            variant=\"link\"\r\n            size=\"sm\"\r\n            className=\"text-nowrap btn-no-border ms-2 ms-md-0\"\r\n            onClick={onCancel}>\r\n            {t('btn_cancel')}\r\n          </Button>\r\n        </div>\r\n      </div>\r\n    </div>\r\n  );\r\n};\r\n\r\nexport default memo(Index);\r\n"
  },
  {
    "path": "ui/src/components/Comment/components/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Form from './Form';\nimport ActionBar from './ActionBar';\nimport Reply from './Reply';\n\nexport { Form, ActionBar, Reply };\n"
  },
  {
    "path": "ui/src/components/Comment/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n@import 'bootstrap/scss/functions';\n@import 'bootstrap/scss/variables';\n@import 'bootstrap/scss/mixins/_breakpoints';\n\n.comments-wrap {\n  .comment-item {\n    &:hover {\n      @include media-breakpoint-up(md) {\n        .control-area {\n          display: flex !important;\n        }\n      }\n    }\n    border-bottom: 1px solid var(--an-comment-item-border-bottom);\n  }\n  .fmt {\n    display: inline;\n    p {\n      &:last-child {\n        display: inline;\n      }\n    }\n    img {\n      display: block;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/components/Comment/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\nimport unionBy from 'lodash/unionBy';\n\nimport * as Types from '@/common/interface';\nimport { Modal } from '@/components';\nimport { usePageUsers, useReportModal, useCaptchaModal } from '@/hooks';\nimport {\n  matchedUsers,\n  parseUserInfo,\n  scrollToElementTop,\n  bgFadeOut,\n} from '@/utils';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport {\n  useQueryComments,\n  addComment,\n  deleteComment,\n  updateComment,\n  postVote,\n} from '@/services';\nimport { commentReplyStore } from '@/stores';\nimport Reactions from '@/pages/Questions/Detail/components/Reactions';\n\nimport { Form, ActionBar, Reply } from './components';\n\nimport './index.scss';\n\ninterface IProps {\n  objectId: string;\n  mode?: 'answer' | 'question';\n  commentId?: string | null;\n  children?: React.ReactNode;\n}\n\nconst Comment: FC<IProps> = ({ objectId, mode, commentId, children }) => {\n  const pageUsers = usePageUsers();\n  const [pageIndex, setPageIndex] = useState(0);\n  const [visibleComment, setVisibleComment] = useState(false);\n  const { id: currentReplyId, update: updateCurrentReplyId } =\n    commentReplyStore();\n  const pageSize = pageIndex === 0 ? 3 : 15;\n  const { data, mutate } = useQueryComments({\n    object_id: objectId,\n    comment_id: commentId,\n    page: pageIndex,\n    page_size: pageSize,\n  });\n  const [comments, setComments] = useState<any>([]);\n\n  const reportModal = useReportModal();\n\n  const addCaptcha = useCaptchaModal('comment');\n  const editCaptcha = useCaptchaPlugin('edit');\n  const dCaptcha = useCaptchaPlugin('delete');\n  const vCaptcha = useCaptchaPlugin('vote');\n\n  const { t } = useTranslation('translation', { keyPrefix: 'comment' });\n\n  useEffect(() => {\n    if (pageIndex === 0 && commentId && comments.length !== 0) {\n      setTimeout(() => {\n        const el = document.getElementById(commentId);\n        scrollToElementTop(el);\n        bgFadeOut(el);\n      }, 100);\n    }\n\n    return () => {\n      updateCurrentReplyId('');\n    };\n  }, [comments]);\n\n  useEffect(() => {\n    if (!data) {\n      return;\n    }\n    if (data.count <= 3) {\n      data.list.sort((a, b) => a.created_at - b.created_at);\n    }\n    if (pageIndex === 1 || pageIndex === 0) {\n      setComments(data?.list);\n    } else {\n      setComments([...comments, ...data.list]);\n    }\n    const user: Types.PageUser[] = [];\n    data.list.forEach((item) => {\n      user.push({\n        id: item.user_id,\n        displayName: item.user_display_name,\n        userName: item.username,\n      });\n      user.push({\n        id: item.reply_comment_id,\n        displayName: item.reply_user_display_name,\n        userName: item.username,\n      });\n    });\n    pageUsers.setUsers(user);\n  }, [data]);\n\n  const handleReply = (id) => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    comments.forEach((item) => {\n      if (item.comment_id === id) {\n        updateCurrentReplyId(id);\n      }\n    });\n  };\n\n  const handleEdit = (id) => {\n    setComments(\n      comments.map((item) => {\n        if (item.comment_id === id) {\n          item.showEdit = !item.showEdit;\n        }\n        return item;\n      }),\n    );\n  };\n\n  const submitUpdateComment = (params, item) => {\n    const up = {\n      ...params,\n      comment_id: item.comment_id,\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    editCaptcha?.resolveCaptchaReq(up);\n\n    return updateComment(up)\n      .then(async (res) => {\n        await editCaptcha?.close();\n        setComments(\n          comments.map((comment) => {\n            if (comment.comment_id === item.comment_id) {\n              comment.showEdit = false;\n              comment.parsed_text = res.parsed_text;\n              comment.original_text = res.original_text;\n            }\n            return comment;\n          }),\n        );\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const captchaErr = editCaptcha?.handleCaptchaError(err.list);\n          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.\n          if (!(captchaErr && err.list.length === 1)) {\n            return Promise.reject(err);\n          }\n        }\n        return Promise.resolve();\n      });\n  };\n\n  const submitAddComment = (params, item) => {\n    const req = {\n      ...params,\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    addCaptcha?.resolveCaptchaReq(req);\n\n    return addComment(req)\n      .then(async (res) => {\n        await addCaptcha?.close();\n        if (item.type === 'reply') {\n          const index = comments.findIndex(\n            (comment) => comment.comment_id === item.comment_id,\n          );\n          updateCurrentReplyId('');\n          comments.splice(index + 1, 0, res);\n          setComments([...comments]);\n        } else {\n          setComments([\n            ...comments.map((comment) => {\n              if (comment.comment_id === item.comment_id) {\n                updateCurrentReplyId('');\n              }\n              return comment;\n            }),\n            res,\n          ]);\n        }\n\n        setVisibleComment(false);\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          const captchaErr = addCaptcha?.handleCaptchaError(ex.list);\n          // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing.\n          if (!(captchaErr && ex.list.length === 1)) {\n            return Promise.reject(ex);\n          }\n        }\n        return Promise.resolve();\n      });\n  };\n\n  const handleSendReply = (item) => {\n    const users = matchedUsers(item.value);\n    const userNames = unionBy(users.map((user) => user.userName));\n    const commentMarkDown = parseUserInfo(item.value);\n\n    const params = {\n      object_id: objectId,\n      original_text: commentMarkDown,\n      mention_username_list: userNames,\n      ...(item.type === 'reply'\n        ? {\n            reply_comment_id: item.comment_id,\n          }\n        : {}),\n    };\n\n    if (item.type === 'edit') {\n      if (!editCaptcha) {\n        return submitUpdateComment(params, item);\n      }\n      return editCaptcha.check(() => submitUpdateComment(params, item));\n    }\n\n    if (!addCaptcha) {\n      return submitAddComment(params, item);\n    }\n\n    return addCaptcha.check(() => submitAddComment(params, item));\n  };\n\n  const submitDeleteComment = (id) => {\n    const imgCode = { captcha_id: undefined, captcha_code: undefined };\n    dCaptcha?.resolveCaptchaReq(imgCode);\n\n    deleteComment(id, imgCode)\n      .then(async () => {\n        await dCaptcha?.close();\n        if (pageIndex === 0) {\n          mutate();\n        }\n        setComments(comments.filter((item) => item.comment_id !== id));\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          dCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n\n  const handleDelete = (id) => {\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'delete' }),\n      content: t('other', { keyPrefix: 'delete' }),\n      confirmBtnVariant: 'danger',\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      onConfirm: () => {\n        if (!dCaptcha) {\n          submitDeleteComment(id);\n          return;\n        }\n        dCaptcha.check(() => {\n          submitDeleteComment(id);\n        });\n      },\n    });\n  };\n\n  const submitVoteComment = (id, is_cancel) => {\n    const imgCode: Types.ImgCodeReq = {\n      captcha_id: undefined,\n      captcha_code: undefined,\n    };\n    vCaptcha?.resolveCaptchaReq(imgCode);\n\n    postVote(\n      {\n        object_id: id,\n        is_cancel,\n        ...imgCode,\n      },\n      'up',\n    )\n      .then(async () => {\n        await vCaptcha?.close();\n        setComments(\n          comments.map((item) => {\n            if (item.comment_id === id) {\n              item.vote_count = is_cancel\n                ? item.vote_count - 1\n                : item.vote_count + 1;\n              item.is_vote = !is_cancel;\n            }\n            return item;\n          }),\n        );\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          vCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n  const handleVote = (id, is_cancel) => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n\n    if (!vCaptcha) {\n      submitVoteComment(id, is_cancel);\n      return;\n    }\n\n    vCaptcha.check(() => {\n      submitVoteComment(id, is_cancel);\n    });\n  };\n\n  const handleAction = ({ action }, item) => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    if (action === 'report') {\n      reportModal.onShow({\n        id: item.comment_id,\n        type: 'comment',\n        action: 'flag',\n      });\n    } else if (action === 'delete') {\n      handleDelete(item.comment_id);\n    } else if (action === 'edit') {\n      handleEdit(item.comment_id);\n    }\n  };\n\n  const handleCancel = (id) => {\n    setComments(\n      comments.map((item) => {\n        if (item.comment_id === id) {\n          item.showEdit = false;\n          updateCurrentReplyId('');\n        }\n        return item;\n      }),\n    );\n  };\n\n  const handleAddComment = () => {\n    if (!tryNormalLogged(true)) {\n      setVisibleComment(false);\n      return;\n    }\n\n    setVisibleComment(!visibleComment);\n  };\n\n  return (\n    <>\n      <div\n        className={classNames(\n          'd-flex flex-wrap justify-content-between align-items-center',\n          comments.length === 0 ? '' : 'mb-3',\n        )}>\n        <Reactions\n          objectId={objectId}\n          showAddCommentBtn={comments.length === 0}\n          handleClickComment={handleAddComment}\n        />\n        {children}\n      </div>\n      <div\n        className={classNames(\n          'comments-wrap',\n          comments.length > 0 && 'bg-light px-3 py-2 rounded',\n        )}>\n        {comments.map((item) => {\n          return (\n            <div\n              key={item.comment_id}\n              id={item.comment_id}\n              className=\"py-2 comment-item\">\n              {item.showEdit ? (\n                <Form\n                  className=\"mt-2\"\n                  value={item.original_text}\n                  type=\"edit\"\n                  mode={mode}\n                  onSendReply={(value) =>\n                    handleSendReply({ ...item, value, type: 'edit' })\n                  }\n                  onCancel={() => handleCancel(item.comment_id)}\n                />\n              ) : (\n                <div className=\"d-block\">\n                  {item.reply_user_display_name &&\n                    (item.reply_user_status !== 'deleted' ? (\n                      <Link\n                        to={`/users/${item.reply_username}`}\n                        className=\"small me-1 text-nowrap\">\n                        @{item.reply_user_display_name}\n                      </Link>\n                    ) : (\n                      <span className=\"small me-1 text-nowrap\">\n                        @{item.reply_user_display_name}\n                      </span>\n                    ))}\n\n                  <div\n                    className=\"fmt small text-break text-wrap\"\n                    dangerouslySetInnerHTML={{ __html: item.parsed_text }}\n                  />\n                </div>\n              )}\n\n              {currentReplyId === item.comment_id ? (\n                <Reply\n                  userName={item.user_display_name}\n                  mode={mode}\n                  onSendReply={(value) =>\n                    handleSendReply({ ...item, value, type: 'reply' })\n                  }\n                  onCancel={() => handleCancel(item.comment_id)}\n                />\n              ) : null}\n              {item.showEdit || currentReplyId === item.comment_id ? null : (\n                <ActionBar\n                  nickName={item.user_display_name}\n                  username={item.username}\n                  createdAt={item.created_at}\n                  voteCount={item.vote_count}\n                  isVote={item.is_vote}\n                  memberActions={item.member_actions}\n                  userStatus={item.user_status}\n                  onReply={() => {\n                    handleReply(item.comment_id);\n                  }}\n                  onAction={(action) => handleAction(action, item)}\n                  onVote={(e) => {\n                    e.preventDefault();\n                    handleVote(item.comment_id, item.is_vote);\n                  }}\n                />\n              )}\n            </div>\n          );\n        })}\n\n        <div className={classNames(comments.length > 0 && 'py-2')}>\n          {comments.length > 0 && (\n            <Button\n              variant=\"link\"\n              className=\"p-0 btn-no-border\"\n              size=\"sm\"\n              onClick={handleAddComment}>\n              {t('btn_add_comment')}\n            </Button>\n          )}\n          {data &&\n            (pageIndex || 1) < Math.ceil((data?.count || 0) / pageSize) && (\n              <Button\n                variant=\"link\"\n                size=\"sm\"\n                className=\"p-0 ms-3 btn-no-border\"\n                onClick={() => {\n                  setPageIndex(pageIndex + 1);\n                }}>\n                {t('show_more', {\n                  count:\n                    data.count - (pageIndex === 0 ? 3 : pageIndex * pageSize),\n                })}\n              </Button>\n            )}\n        </div>\n\n        {visibleComment && (\n          <Form\n            mode={mode}\n            className={classNames(\n              comments.length <= 0 ? 'mt-3' : 'mt-2',\n              comments.length <= 0 && 'bg-light p-3 rounded',\n            )}\n            onSendReply={(value) => handleSendReply({ value, type: 'comment' })}\n            onCancel={() => setVisibleComment(!visibleComment)}\n          />\n        )}\n      </div>\n    </>\n  );\n};\n\nexport default Comment;\n"
  },
  {
    "path": "ui/src/components/Counts/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport classname from 'classnames';\n\nimport { Icon } from '@/components';\nimport { formatCount } from '@/utils/common';\n\ninterface Props {\n  data: {\n    votes: number;\n    answers: number;\n    views: number;\n  };\n  showVotes?: boolean;\n  showAnswers?: boolean;\n  showViews?: boolean;\n  showAccepted?: boolean;\n  isAccepted?: boolean;\n  className?: string;\n}\nconst Index: FC<Props> = ({\n  data,\n  showVotes = true,\n  showAnswers = true,\n  showViews = true,\n  isAccepted = false,\n  showAccepted = false,\n  className = '',\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'counts' });\n\n  return (\n    <div className={classname('d-flex align-items-center', className)}>\n      {showVotes && (\n        <div className=\"d-flex align-items-center flex-shrink-0 text-body\">\n          <Icon name=\"hand-thumbs-up-fill me-1\" />\n          <span className=\"fw-medium\">{data.votes}</span>\n          <span className=\"ms-1\">{t('votes')}</span>\n        </div>\n      )}\n\n      {showAccepted && (\n        <div className=\"d-flex align-items-center ms-3 text-success flex-shrink-0\">\n          <Icon name=\"check-circle-fill me-1\" />\n          <span>{t('accepted')}</span>\n        </div>\n      )}\n\n      {showAnswers && (\n        <div\n          className={`d-flex flex-shrink-0 align-items-center ms-3 ${\n            isAccepted ? 'text-bg-success rounded-pill px-2 ' : ''\n          }`}>\n          {isAccepted ? (\n            <Icon name=\"check-circle-fill me-1\" />\n          ) : (\n            <Icon name=\"chat-square-text-fill me-1\" />\n          )}\n          <span className=\"fw-medium\">{data.answers}</span>\n          <span className=\"ms-1\">{t('answers')}</span>\n        </div>\n      )}\n      {showViews && (\n        <span\n          className={classname(\n            'summary-stat ms-3 flex-shrink-0',\n            data.views >= 100 * 1000\n              ? 'view-level3'\n              : data.views >= 10000\n                ? 'view-level2'\n                : data.views >= 1000\n                  ? 'view-level1'\n                  : '',\n          )}>\n          <Icon name=\"bar-chart-fill\" />\n          <span className=\"fw-medium ms-1\">{formatCount(data.views)}</span>\n          <span className=\"ms-1\">{t('views')}</span>\n        </span>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/CustomSidebar/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\n\nimport { customizeStore } from '@/stores';\n\nconst Index = () => {\n  const { custom_sidebar } = customizeStore((state) => state);\n  if (!custom_sidebar) return null;\n  return <div dangerouslySetInnerHTML={{ __html: custom_sidebar }} />;\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Customize/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nimport { customizeStore } from '@/stores';\n\nconst CUSTOM_MARK_HEAD = 'customize_head';\nconst CUSTOM_MARK_HEADER = 'customize_header';\nconst CUSTOM_MARK_FOOTER = 'customize_footer';\n\nconst makeMarker = (mark) => {\n  return `<!--${mark}-->`;\n};\n\nconst ActivateScriptNodes = (el, part) => {\n  let startMarkNode;\n  const scriptList: HTMLScriptElement[] = [];\n  const { childNodes } = el;\n  for (let i = 0; i < childNodes.length; i += 1) {\n    const node = childNodes[i];\n    if (node.nodeType === 8 && node.nodeValue === part) {\n      if (!startMarkNode) {\n        startMarkNode = node;\n      } else {\n        // this is the endMarkNode\n        break;\n      }\n    }\n    if (\n      startMarkNode &&\n      node.nodeType === 1 &&\n      node.nodeName.toLowerCase() === 'script'\n    ) {\n      scriptList.push(node);\n    }\n  }\n  scriptList?.forEach((so) => {\n    const script = document.createElement('script');\n    script.text = `(() => {${so.text}})();`;\n    for (let i = 0; i < so.attributes.length; i += 1) {\n      const attr = so.attributes[i];\n      script.setAttribute(attr.name, attr.value);\n    }\n    el.replaceChild(script, so);\n  });\n};\n\ntype pos = 'afterbegin' | 'beforeend';\nconst renderCustomArea = (el, part, pos: pos, content: string = '') => {\n  let startMarkNode;\n  let endMarkNode;\n  const { childNodes } = el;\n  for (let i = 0; i < childNodes.length; i += 1) {\n    const node = childNodes[i];\n    if (node.nodeType === 8 && node.nodeValue === part) {\n      if (!startMarkNode) {\n        startMarkNode = node;\n      } else {\n        endMarkNode = node;\n        break;\n      }\n    }\n  }\n\n  if (startMarkNode && endMarkNode) {\n    while (\n      startMarkNode.nextSibling &&\n      startMarkNode.nextSibling !== endMarkNode\n    ) {\n      el.removeChild(startMarkNode.nextSibling);\n    }\n  }\n  if (startMarkNode) {\n    el.removeChild(startMarkNode);\n  }\n  if (endMarkNode) {\n    el.removeChild(endMarkNode);\n  }\n  el.insertAdjacentHTML(pos, makeMarker(part));\n  el.insertAdjacentHTML(pos, content);\n  el.insertAdjacentHTML(pos, makeMarker(part));\n  ActivateScriptNodes(el, part);\n};\nconst handleCustomHead = (content) => {\n  const el = document.head;\n  renderCustomArea(el, CUSTOM_MARK_HEAD, 'beforeend', content);\n};\n\nconst handleCustomHeader = (content) => {\n  const el = document.body;\n  renderCustomArea(el, CUSTOM_MARK_HEADER, 'afterbegin', content);\n};\n\nconst handleCustomFooter = (content) => {\n  const el = document.body;\n  renderCustomArea(el, CUSTOM_MARK_FOOTER, 'beforeend', content);\n};\n\nconst Index: FC = () => {\n  const { custom_head, custom_header, custom_footer } = customizeStore(\n    (state) => state,\n  );\n  const { pathname } = useLocation();\n\n  useEffect(() => {\n    const isSeo = document.querySelector('meta[name=\"go-template\"]');\n    if (!isSeo) {\n      setTimeout(() => {\n        handleCustomHead(custom_head);\n      }, 1000);\n      handleCustomHeader(custom_header);\n      handleCustomFooter(custom_footer);\n    } else {\n      isSeo.remove();\n    }\n  }, [custom_head, custom_header, custom_footer]);\n\n  useEffect(() => {\n    /**\n     * description:  Activate scripts with data-client attribute when route changes\n     */\n    const allScript = document.body.querySelectorAll('script[data-client]');\n    allScript.forEach((scriptNode) => {\n      const script = document.createElement('script');\n      script.setAttribute('data-client', 'true');\n      // If the script is already wrapped in an IIFE, use it directly; otherwise, wrap it in an IIFE\n      if (\n        /^\\s*\\(\\s*function\\s*\\(\\s*\\)\\s*{/.test(\n          (scriptNode as HTMLScriptElement).text,\n        ) ||\n        /^\\s*\\(\\s*\\(\\s*\\)\\s*=>\\s*{/.test((scriptNode as HTMLScriptElement).text)\n      ) {\n        script.text = (scriptNode as HTMLScriptElement).text;\n      } else {\n        script.text = `(() => {${(scriptNode as HTMLScriptElement).text}})();`;\n      }\n      for (let i = 0; i < scriptNode.attributes.length; i += 1) {\n        const attr = scriptNode.attributes[i];\n        if (attr.name !== 'data-client') {\n          script.setAttribute(attr.name, attr.value);\n        }\n      }\n      scriptNode.parentElement?.replaceChild(script, scriptNode);\n    });\n  }, [pathname]);\n\n  return null;\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/CustomizeTheme/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useLayoutEffect } from 'react';\nimport { Helmet } from 'react-helmet-async';\n\nimport Color from 'color';\n\nimport { shiftColor, tintColor, shadeColor } from '@/utils';\nimport { themeSettingStore } from '@/stores';\nimport { DEFAULT_THEME_COLOR } from '@/common/constants';\n\nconst Index: FC = () => {\n  const { theme, theme_config } = themeSettingStore((_) => _);\n  let primaryColor;\n  if (theme_config?.[theme]?.primary_color) {\n    primaryColor = Color(theme_config[theme].primary_color);\n  }\n  const setThemeColor = () => {\n    const themeMetaNode = document.querySelector('meta[name=\"theme-color\"]');\n    if (themeMetaNode) {\n      const themeColor = primaryColor\n        ? primaryColor.hex()\n        : DEFAULT_THEME_COLOR;\n      themeMetaNode.setAttribute('content', themeColor);\n    }\n  };\n  useLayoutEffect(() => {\n    setThemeColor();\n  }, [primaryColor]);\n\n  return (\n    <Helmet>\n      {primaryColor && (\n        <style>\n          {`\n              :root {\n                --bs-blue: ${primaryColor.hex()};\n                --bs-primary: ${primaryColor.hex()};\n                --bs-primary-rgb: ${primaryColor.rgb().array().join(',')};\n                --bs-link-color: ${primaryColor.hex()};\n                --bs-link-color-rgb: ${primaryColor.rgb().array().join(',')};\n                --bs-link-hover-color: ${shiftColor(primaryColor, 0.8).hex()};\n                --bs-link-hover-color-rgb: ${shiftColor(primaryColor, 0.8)\n                  .round()\n                  .array()}\n              }\n              :root[data-bs-theme='dark'] {\n                --bs-link-color: ${tintColor(primaryColor, 0.6).hex()};\n                --bs-link-color-rgb: ${tintColor(primaryColor, 0.6)\n                  .round()\n                  .array()};\n                --bs-link-hover-color: ${shiftColor(\n                  tintColor(primaryColor, 0.6),\n                  -0.8,\n                ).hex()};\n                --bs-link-hover-color-rgb: ${shiftColor(\n                  tintColor(primaryColor, 0.6),\n                  -0.8,\n                )\n                  .round()\n                  .array()};\n              }\n              .nav-pills {\n                --bs-nav-pills-link-active-bg: ${primaryColor.hex()};\n              }\n              .btn-primary {\n                --bs-btn-bg: ${primaryColor.hex()};\n                --bs-btn-border-color: ${primaryColor.hex()};\n                --bs-btn-hover-bg: ${tintColor(primaryColor, 0.85)};\n                --bs-btn-hover-border-color: ${tintColor(primaryColor, 0.9)};\n                --bs-btn-focus-shadow-rgb: ${shadeColor(primaryColor, 0.85)};\n                --bs-btn-active-bg: ${tintColor(primaryColor, 0.8)};\n                --bs-btn-active-border-color: ${tintColor(primaryColor, 0.9)};\n                --bs-btn-disabled-bg: ${primaryColor.hex()};\n                --bs-btn-disabled-border-color: ${primaryColor.hex()};\n              }\n              .btn-outline-primary {\n                --bs-btn-color: ${primaryColor.hex()};\n                --bs-btn-border-color: ${primaryColor.hex()};\n                --bs-btn-hover-bg: ${primaryColor.hex()};\n                --bs-btn-hover-border-color: ${primaryColor.hex()};\n                --bs-btn-active-bg: ${primaryColor.hex()};\n                --bs-btn-active-border-color: ${primaryColor.hex()};\n                --bs-btn-disabled-color: ${primaryColor.hex()};\n                --bs-btn-disabled-border-color: ${primaryColor.hex()};\n              }\n              .pagination {\n                --bs-btn-color: ${primaryColor.hex()};\n                --bs-pagination-active-bg: ${primaryColor.hex()};\n                --bs-pagination-active-border-color: ${primaryColor.hex()};\n              }\n              .form-select:focus,\n              .form-control:focus,\n               .form-control.focus{\n                box-shadow: 0 0 0 0.25rem ${primaryColor\n                  .fade(0.75)\n                  .string()} !important;\n                border-color: ${tintColor(primaryColor, 0.5)} !important;\n              }\n              .form-check-input:checked {\n                background-color: ${primaryColor.hex()};\n                border-color: ${primaryColor.hex()};\n              }\n              .form-check-input:focus {\n                border-color: ${tintColor(primaryColor, 0.5)};\n                box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), .4);\n              }\n              .form-switch .form-check-input:focus {\n                background-image: url(\"data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3e%3ccircle r=%273%27 fill=%27${tintColor(\n                  primaryColor,\n                  0.5,\n                )}%27/%3e%3c/svg%3e\");\n              }\n              .tag-selector-wrap--focus {\n                box-shadow: 0 0 0 0.25rem ${primaryColor\n                  .fade(0.75)\n                  .string()} !important;\n                border-color: ${tintColor(primaryColor, 0.5)} !important;\n              }\n              .dropdown-menu {\n                --bs-dropdown-link-active-bg: rgb(var(--bs-primary-rgb));\n              }\n              .link-primary {\n                color: ${primaryColor.hex()}!important;\n              }\n              .link-primary:hover, .link-primary:focus {\n                color: ${shadeColor(primaryColor, 0.8).hex()}!important;\n              }\n\n            `}\n        </style>\n      )}\n    </Helmet>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/DiffContent/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\n\nimport classnames from 'classnames';\n\nimport { Tag } from '@/components';\nimport { diffText } from '@/utils';\n\ninterface Props {\n  objectType: string | 'question' | 'answer' | 'tag';\n  newData: Record<string, any>;\n  oldData?: Record<string, any>;\n  className?: string;\n  opts?: Partial<{\n    showTitle: boolean;\n    showTagUrlSlug: boolean;\n  }>;\n}\n\nconst Index: FC<Props> = ({\n  objectType,\n  newData,\n  oldData,\n  className = '',\n  opts = {\n    showTitle: true,\n    showTagUrlSlug: true,\n  },\n}) => {\n  if (!newData) return null;\n\n  let tag = newData.tags;\n  if (objectType === 'question' && oldData?.tags) {\n    const addTags = newData.tags.filter(\n      (c) => !oldData?.tags?.find((p) => p.slug_name === c.slug_name),\n    );\n\n    let deleteTags = oldData?.tags\n      .filter((c) => !newData?.tags.find((p) => p.slug_name === c.slug_name))\n      .map((v) => ({ ...v, state: 'delete' }));\n\n    deleteTags = deleteTags?.map((v) => {\n      const index = oldData?.tags?.findIndex(\n        (c) => c.slug_name === v.slug_name,\n      );\n      return {\n        ...v,\n        pre_index: index,\n      };\n    });\n\n    tag = newData.tags.map((item) => {\n      const find = addTags.find((c) => c.slug_name === item.slug_name);\n      if (find) {\n        return {\n          ...find,\n          state: 'add',\n        };\n      }\n      return item;\n    });\n\n    deleteTags.forEach((v) => {\n      tag.splice(v.pre_index, 0, v);\n    });\n  }\n\n  return (\n    <div className={className}>\n      {objectType !== 'answer' && opts?.showTitle && (\n        <h5\n          dangerouslySetInnerHTML={{\n            __html: diffText(\n              newData.title?.replace(/</gi, '&lt;'),\n              oldData?.title?.replace(/</gi, '&lt;'),\n            ),\n          }}\n          className=\"mb-3\"\n        />\n      )}\n      {objectType === 'question' && (\n        <div className=\"mb-4\">\n          {tag?.map((item) => {\n            return (\n              <Tag\n                key={item.slug_name}\n                className=\"me-1\"\n                data={item}\n                textClassName={`d-inline-block review-text-${item.state}`}\n              />\n            );\n          })}\n        </div>\n      )}\n      {objectType === 'tag' && opts?.showTagUrlSlug && (\n        <div\n          className={classnames(\n            'small font-monospace',\n            newData.original_text && 'mb-4',\n          )}\n          dangerouslySetInnerHTML={{\n            __html: `/tags/${\n              newData?.main_tag_slug_name\n                ? diffText(\n                    newData.main_tag_slug_name,\n                    oldData?.main_tag_slug_name,\n                  )\n                : diffText(newData.slug_name, oldData?.slug_name)\n            }`,\n          }}\n        />\n      )}\n      <div\n        dangerouslySetInnerHTML={{\n          __html: diffText(newData.original_text, oldData?.original_text),\n        }}\n        className=\"pre-line text-break font-monospace small\"\n      />\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Editor/EditorContext.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React from 'react';\n\nimport { Editor } from './types';\n\nexport const EditorContext = React.createContext<Editor | null>(null);\n"
  },
  {
    "path": "ui/src/components/Editor/MarkdownEditor.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useRef } from 'react';\n\nimport { EditorView } from '@codemirror/view';\n\nimport { BaseEditorProps } from './types';\nimport { useEditor } from './utils';\n\ninterface MarkdownEditorProps extends BaseEditorProps {}\n\nconst MarkdownEditor: React.FC<MarkdownEditorProps> = ({\n  value,\n  onChange,\n  onFocus,\n  onBlur,\n  placeholder,\n  autoFocus,\n  onEditorReady,\n}) => {\n  const editorRef = useRef<HTMLDivElement>(null);\n  const lastSyncedValueRef = useRef<string>(value);\n  const isInitializedRef = useRef<boolean>(false);\n\n  const editor = useEditor({\n    editorRef,\n    onChange,\n    onFocus,\n    onBlur,\n    placeholder,\n    autoFocus,\n    initialValue: value,\n  });\n\n  useEffect(() => {\n    if (!editor || isInitializedRef.current) {\n      return;\n    }\n\n    isInitializedRef.current = true;\n    onEditorReady?.(editor);\n  }, [editor, onEditorReady]);\n\n  useEffect(() => {\n    if (!editor || value === lastSyncedValueRef.current) {\n      return;\n    }\n\n    const currentValue = editor.getValue();\n    if (currentValue !== value) {\n      editor.setValue(value || '');\n      lastSyncedValueRef.current = value || '';\n    }\n  }, [editor, value]);\n\n  useEffect(() => {\n    lastSyncedValueRef.current = value;\n    isInitializedRef.current = false;\n\n    return () => {\n      if (editor) {\n        const view = editor as unknown as EditorView;\n        if (view.destroy) {\n          view.destroy();\n        }\n      }\n      isInitializedRef.current = false;\n    };\n  }, []);\n\n  return (\n    <div className=\"content-wrap\">\n      <div\n        className=\"md-editor position-relative w-100 h-100\"\n        ref={editorRef}\n      />\n    </div>\n  );\n};\n\nexport default MarkdownEditor;\n"
  },
  {
    "path": "ui/src/components/Editor/Select/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Dropdown, FormControl } from 'react-bootstrap';\n\ninterface IProps {\n  options;\n  value?;\n  onChange?;\n  placeholder?;\n  onSelect?;\n}\nconst Select: FC<IProps> = ({\n  options = [],\n  value = '',\n  onChange,\n  placeholder = '',\n  onSelect,\n}) => {\n  const [isFocus, setFocusState] = useState(false);\n  const [cursor, setCursor] = useState(0);\n\n  useEffect(() => {\n    setCursor(0);\n  }, [value]);\n  const handleKeyDown = (e) => {\n    const { keyCode } = e;\n\n    if (keyCode === 38 && cursor > 0) {\n      e.preventDefault();\n      setCursor(cursor - 1);\n    }\n    if (keyCode === 40 && cursor < options.length - 1) {\n      e.preventDefault();\n\n      setCursor(cursor + 1);\n    }\n    if (keyCode === 13 && cursor > -1 && cursor <= options.length - 1) {\n      const lang = options.filter((opt) =>\n        value ? opt.indexOf(value) === 0 : true,\n      )[cursor];\n\n      setFocusState(false);\n      onSelect(lang);\n    }\n  };\n\n  const result = options.filter((opt) =>\n    value ? opt.indexOf(value) === 0 : true,\n  );\n\n  return (\n    <div className=\"position-relative\" onKeyDown={handleKeyDown}>\n      <FormControl\n        type=\"search\"\n        value={value}\n        placeholder={placeholder}\n        onChange={(e) => {\n          setFocusState(true);\n          if (onChange instanceof Function) {\n            onChange(e);\n          }\n        }}\n      />\n      {result.length > 0 && (\n        <Dropdown.Menu\n          show={value && isFocus}\n          className=\"border py-2 rounded w-100\"\n          style={{ overflowY: 'auto', maxHeight: '250px' }}>\n          {result.map((opt, index) => {\n            return (\n              <Dropdown.Item\n                key={opt}\n                className={`${cursor === index ? 'active' : ''}`}\n                onClick={(e) => {\n                  e.preventDefault();\n                  setFocusState(false);\n                  onSelect(opt);\n                }}>\n                {opt}\n              </Dropdown.Item>\n            );\n          })}\n        </Dropdown.Menu>\n      )}\n    </div>\n  );\n};\n\nexport default Select;\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/blockquote.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst BlockQuote = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n\n  const item = {\n    label: 'quote',\n    keyMap: ['Ctrl-q'],\n    tip: `${t('blockquote.text')} (Ctrl+Q)`,\n  };\n\n  const handleClick = (editor: Editor) => {\n    editor.insertBlockquote(t('blockquote.text'));\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(BlockQuote);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/bold.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Bold = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'type-bold',\n    keyMap: ['Ctrl-b'],\n    tip: `${t('bold.text')} (Ctrl+b)`,\n  };\n  const DEFAULTTEXT = t('bold.text');\n\n  const handleClick = (editor: Editor) => {\n    editor.insertBold(DEFAULTTEXT);\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Bold);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/code.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useRef, useState, memo } from 'react';\nimport { Button, Form, Modal } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport Select from '../Select';\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst codeLanguageType = [\n  'bash',\n  'sh',\n  'zsh',\n  'c',\n  'h',\n  'cpp',\n  'hpp',\n  'c++',\n  'h++',\n  'cc',\n  'hh',\n  'cxx',\n  'hxx',\n  'c-like',\n  'cs',\n  'csharp',\n  'c#',\n  'clojure',\n  'clj',\n  'coffee',\n  'coffeescript',\n  'cson',\n  'iced',\n  'css',\n  'dart',\n  'erl',\n  'erlang',\n  'go',\n  'golang',\n  'hs',\n  'haskell',\n  'html',\n  'xml',\n  'xsl',\n  'xhtml',\n  'rss',\n  'atom',\n  'xjb',\n  'xsd',\n  'plist',\n  'wsf',\n  'svg',\n  'http',\n  'https',\n  'ini',\n  'toml',\n  'java',\n  'jsp',\n  'js',\n  'javascript',\n  'jsx',\n  'mjs',\n  'cjs',\n  'json',\n  'kotlin',\n  'kt',\n  'latex',\n  'tex',\n  'less',\n  'lisp',\n  'lua',\n  'makefile',\n  'mk',\n  'mak',\n  'markdown',\n  'md',\n  'mkdown',\n  'mkd',\n  'matlab',\n  'objectivec',\n  'mm',\n  'objc',\n  'obj-c',\n  'ocaml',\n  'ml',\n  'pascal',\n  'delphi',\n  'dpr',\n  'dfm',\n  'pas',\n  'freepascal',\n  'lazarus',\n  'lpr',\n  'lfm',\n  'pl',\n  'perl',\n  'pm',\n  'php',\n  'php3',\n  'php4',\n  'php5',\n  'php6',\n  'php7',\n  'php-template',\n  'protobuf',\n  'py',\n  'python',\n  'gyp',\n  'ipython',\n  'r',\n  'rb',\n  'ruby',\n  'gemspec',\n  'podspec',\n  'thor',\n  'irb',\n  'rs',\n  'rust',\n  'scala',\n  'scheme',\n  'scss',\n  'shell',\n  'console',\n  'sql',\n  'swift',\n  'typescript',\n  'ts',\n  'vhdl',\n  'vbnet',\n  'vb',\n  'yaml',\n  'yml',\n];\n\nconst Code = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n\n  const item = {\n    label: 'code-slash',\n    keyMap: ['Ctrl-k'],\n    tip: `${t('code.text')} (Ctrl+k)`,\n  };\n\n  const [code, setCode] = useState({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  });\n  const [visible, setVisible] = useState(false);\n  const [lang, setLang] = useState('');\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const SINGLELINEMAXLENGTH = 40;\n  const [currentEditor, setCurrentEditor] = useState<Editor | null>(null);\n\n  const addCode = (editor: Editor) => {\n    setCurrentEditor(editor);\n    const text = editor.getSelection();\n\n    if (!text) {\n      setVisible(true);\n      return;\n    }\n    if (text.length > SINGLELINEMAXLENGTH) {\n      editor.insertCodeBlock('', text);\n    } else {\n      editor.insertCode(text);\n    }\n    editor.focus();\n  };\n\n  useEffect(() => {\n    if (visible && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [visible]);\n\n  const handleClick = () => {\n    if (!currentEditor) {\n      return;\n    }\n\n    if (!code.value.trim()) {\n      setCode({\n        ...code,\n        errorMsg: t('code.form.fields.code.msg.empty'),\n        isInvalid: true,\n      });\n      return;\n    }\n\n    if (\n      code.value.split('\\n').length > 1 ||\n      code.value.length >= SINGLELINEMAXLENGTH\n    ) {\n      currentEditor.insertCodeBlock(lang || undefined, code.value);\n    } else {\n      currentEditor.insertCode(code.value);\n    }\n\n    setCode({\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    });\n    setLang('');\n    setVisible(false);\n    currentEditor.focus();\n  };\n  const onHide = () => setVisible(false);\n  const onExited = () => currentEditor?.focus();\n\n  return (\n    <ToolItem {...item} onClick={addCode}>\n      <Modal\n        show={visible}\n        onHide={onHide}\n        onExited={onExited}\n        fullscreen=\"sm-down\">\n        <Modal.Header closeButton>\n          <h5 className=\"mb-0\">{t('code.add_code')}</h5>\n        </Modal.Header>\n        <Modal.Body>\n          <Form.Group controlId=\"editor.code\" className=\"mb-3\">\n            <Form.Label>{t('code.form.fields.code.label')}</Form.Label>\n            <Form.Control\n              ref={inputRef}\n              as=\"textarea\"\n              rows={3}\n              value={code.value}\n              isInvalid={code.isInvalid}\n              className=\"font-monospace\"\n              style={{ height: '200px' }}\n              onChange={(e) => setCode({ ...code, value: e.target.value })}\n            />\n            {code.isInvalid && (\n              <Form.Control.Feedback type=\"invalid\">\n                {code.errorMsg}\n              </Form.Control.Feedback>\n            )}\n          </Form.Group>\n          <Form.Group controlId=\"editor.codeLanguageType\" className=\"mb-3\">\n            <Form.Label>{`${t('code.form.fields.language.label')} ${t(\n              'optional',\n              {\n                keyPrefix: 'form',\n              },\n            )}`}</Form.Label>\n            <Select\n              options={codeLanguageType}\n              value={lang}\n              onChange={(e) => setLang(e.target.value)}\n              onSelect={(val) => setLang(val)}\n              placeholder={t('code.form.fields.language.placeholder')}\n            />\n          </Form.Group>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button\n            variant=\"link\"\n            onClick={() => {\n              setVisible(false);\n              setCode({\n                value: '',\n                isInvalid: false,\n                errorMsg: '',\n              });\n            }}>\n            {t('code.btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleClick}>\n            {t('code.btn_confirm')}\n          </Button>\n        </Modal.Footer>\n      </Modal>\n    </ToolItem>\n  );\n};\n\nexport default memo(Code);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/file.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, useRef, useContext } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal as AnswerModal } from '@/components';\nimport ToolItem from '../toolItem';\nimport { EditorContext } from '../EditorContext';\nimport { uploadImage } from '@/services';\nimport { writeSettingStore } from '@/stores';\n\nconst File = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const { max_attachment_size = 8, authorized_attachment_extensions = [] } =\n    writeSettingStore((state) => state.write);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const editor = useContext(EditorContext);\n\n  const item = {\n    label: 'paperclip',\n    tip: `${t('file.text')}`,\n  };\n\n  const addLink = () => {\n    fileInputRef.current?.click?.();\n  };\n\n  const verifyFileSize = (files: FileList) => {\n    if (files.length === 0) {\n      return false;\n    }\n    const unSupportFiles = Array.from(files).filter((file) => {\n      const fileName = file.name.toLowerCase();\n      return !authorized_attachment_extensions.find((v) =>\n        fileName.endsWith(v),\n      );\n    });\n\n    if (unSupportFiles.length > 0) {\n      AnswerModal.confirm({\n        content: t('file.not_supported', {\n          file_type: authorized_attachment_extensions.join(', '),\n        }),\n        showCancel: false,\n      });\n      return false;\n    }\n\n    const attachmentOverSizeFiles = Array.from(files).filter(\n      (file) => file.size / 1024 / 1024 > max_attachment_size,\n    );\n    if (attachmentOverSizeFiles.length > 0) {\n      AnswerModal.confirm({\n        content: t('file.max_size', { size: max_attachment_size }),\n        showCancel: false,\n      });\n      return false;\n    }\n\n    return true;\n  };\n\n  const onUpload = async (e) => {\n    if (!editor) {\n      return;\n    }\n    const files = e.target?.files || [];\n    const bool = verifyFileSize(files);\n\n    if (!bool) {\n      return;\n    }\n    const fileName = files[0].name;\n    const loadingText = `![${t('image.uploading')} ${fileName}...]()`;\n    const startPos = editor.getCursor();\n\n    const endPos = { ...startPos, ch: startPos.ch + loadingText.length };\n    editor.replaceSelection(loadingText);\n    editor.setReadOnly(true);\n\n    uploadImage({ file: e.target.files[0], type: 'post_attachment' })\n      .then((url) => {\n        const text = `[${fileName}](${url})`;\n        editor.replaceRange('', startPos, endPos);\n        editor.replaceSelection(text);\n      })\n      .catch(() => {\n        editor.replaceRange('', startPos, endPos);\n      })\n      .finally(() => {\n        editor.setReadOnly(false);\n        editor.focus();\n      });\n  };\n\n  if (!authorized_attachment_extensions?.length) {\n    return null;\n  }\n\n  return (\n    <ToolItem {...item} onClick={addLink}>\n      <input\n        type=\"file\"\n        className=\"d-none\"\n        accept={`.${authorized_attachment_extensions\n          .join(',.')\n          .toLocaleLowerCase()}`}\n        ref={fileInputRef}\n        onChange={onUpload}\n      />\n    </ToolItem>\n  );\n};\n\nexport default memo(File);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/heading.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, memo } from 'react';\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor, Level } from '../types';\n\nconst Heading = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const headerList = [\n    {\n      text: `<h2 class=\"mb-0 h4\">${t('heading.options.h2')}</h2>`,\n      level: 2,\n      label: t('heading.options.h2'),\n    },\n    {\n      text: `<h3 class=\"mb-0 h5\">${t('heading.options.h3')}</h3>`,\n      level: 3,\n      label: t('heading.options.h3'),\n    },\n    {\n      text: `<h4 class=\"mb-0 h6\">${t('heading.options.h4')}</h4>`,\n      level: 4,\n      label: t('heading.options.h4'),\n    },\n    {\n      text: `<h5 class=\"mb-0 small\">${t('heading.options.h5')}</h5>`,\n      level: 5,\n      label: t('heading.options.h5'),\n    },\n    {\n      text: `<h6 class=\"mb-0 fs-12\">${t('heading.options.h6')}</h6>`,\n      level: 6,\n      label: t('heading.options.h6'),\n    },\n  ];\n  const item = {\n    label: 'type-h2',\n    keyMap: ['Ctrl-h'],\n    tip: `${t('heading.text')} (Ctrl+h)`,\n  };\n  const [isShow, setShowState] = useState(false);\n  const [isLocked, setLockState] = useState(false);\n  const [currentEditor, setCurrentEditor] = useState<Editor | null>(null);\n\n  const handleClick = (level: Level = 2, label?: string) => {\n    if (!currentEditor) {\n      return;\n    }\n    currentEditor.insertHeading(level, label);\n    currentEditor.focus();\n    setShowState(false);\n  };\n  const onAddHeader = (editor: Editor) => {\n    setCurrentEditor(editor);\n    if (isLocked) {\n      return;\n    }\n    setShowState(!isShow);\n  };\n\n  const handleMouseEnter = () => {\n    setLockState(true);\n  };\n\n  const handleMouseLeave = () => {\n    setLockState(false);\n  };\n  return (\n    <ToolItem\n      as=\"dropdown\"\n      {...item}\n      isShow={isShow}\n      onClick={onAddHeader}\n      onBlur={onAddHeader}>\n      <Dropdown.Menu\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}>\n        {headerList.map((header) => {\n          return (\n            <Dropdown.Item\n              key={header.text}\n              onClick={(e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                handleClick(header.level as Level, header.label);\n              }}\n              dangerouslySetInnerHTML={{ __html: header.text }}\n            />\n          );\n        })}\n      </Dropdown.Menu>\n    </ToolItem>\n  );\n};\n\nexport default memo(Heading);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/help.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\n\nconst Help = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n\n  const item = {\n    label: 'question-circle-fill',\n    tip: t('help.text'),\n  };\n  const handleClick = () => {\n    window.open('https://commonmark.org/help/');\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Help);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/hr.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Hr = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'hr',\n    keyMap: ['Ctrl-r'],\n    tip: `${t('hr.text')} (Ctrl+r)`,\n  };\n  const handleClick = (editor: Editor) => {\n    editor.insertHorizontalRule();\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Hr);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/image.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState, memo, useContext } from 'react';\nimport { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { EditorContext } from '../EditorContext';\nimport { Editor } from '../types';\nimport { useImageUpload } from '../hooks/useImageUpload';\n\nconst Image = () => {\n  const editor = useContext(EditorContext);\n  const [editorState, setEditorState] = useState<Editor | null>(editor);\n\n  // Update editor state when editor context changes\n  // This ensures event listeners are re-bound when switching editor modes\n  useEffect(() => {\n    if (editor) {\n      setEditorState(editor);\n    }\n  }, [editor]);\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const { verifyImageSize, uploadFiles } = useImageUpload();\n\n  const loadingText = `![${t('image.uploading')}...]()`;\n\n  const item = {\n    label: 'image-fill',\n    keyMap: ['Ctrl-g'],\n    tip: `${t('image.text')} (Ctrl+G)`,\n  };\n  const [currentTab, setCurrentTab] = useState('localImage');\n  const [visible, setVisible] = useState(false);\n  const [link, setLink] = useState({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n    type: '',\n  });\n\n  const [imageName, setImageName] = useState({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  });\n\n  function dragenter(e) {\n    e.stopPropagation();\n    e.preventDefault();\n  }\n\n  function dragover(e) {\n    e.stopPropagation();\n    e.preventDefault();\n  }\n  const drop = async (e) => {\n    const fileList = e.dataTransfer.files;\n    const bool = verifyImageSize(fileList);\n\n    if (!bool) {\n      return;\n    }\n\n    if (!editorState) {\n      return;\n    }\n    const startPos = editorState.getCursor();\n\n    const endPos = { ...startPos, ch: startPos.ch + loadingText.length };\n\n    editorState.replaceSelection(loadingText);\n    editorState.setReadOnly(true);\n    const urls = await uploadFiles(fileList)\n      .catch(() => {\n        editorState.replaceRange('', startPos, endPos);\n      })\n      .finally(() => {\n        editorState?.setReadOnly(false);\n        editorState?.focus();\n      });\n\n    const text: string[] = [];\n    if (Array.isArray(urls)) {\n      urls.forEach(({ name, url, type }) => {\n        if (name && url) {\n          text.push(`${type === 'post' ? '!' : ''}[${name}](${url})`);\n        }\n      });\n    }\n    if (text.length) {\n      editorState.replaceRange(text.join('\\n'), startPos, endPos);\n    } else {\n      editorState?.replaceRange('', startPos, endPos);\n    }\n  };\n\n  const paste = async (event) => {\n    const clipboard = event.clipboardData;\n\n    const bool = verifyImageSize(clipboard.files);\n\n    if (bool) {\n      event.preventDefault();\n      if (!editorState) {\n        return;\n      }\n      const startPos = editorState.getCursor();\n      const endPos = { ...startPos, ch: startPos.ch + loadingText.length };\n\n      editorState?.replaceSelection(loadingText);\n      editorState?.setReadOnly(true);\n      uploadFiles(clipboard.files)\n        .then((urls) => {\n          const text = urls.map(({ name, url, type }) => {\n            return `${type === 'post' ? '!' : ''}[${name}](${url})`;\n          });\n\n          editorState.replaceRange(text.join('\\n'), startPos, endPos);\n        })\n        .catch(() => {\n          editorState.replaceRange('', startPos, endPos);\n        })\n        .finally(() => {\n          editorState?.setReadOnly(false);\n          editorState?.focus();\n        });\n\n      return;\n    }\n\n    const htmlStr = clipboard.getData('text/html');\n    const imgRegex = /<img([\\s\\S]*?) src\\s*=\\s*(['\"])([\\s\\S]*?)\\2([^>]*)>/;\n\n    if (!htmlStr.match(imgRegex)) {\n      return;\n    }\n    event.preventDefault();\n    const parser = new DOMParser();\n    const doc = parser.parseFromString(htmlStr, 'text/html');\n    const { body } = doc;\n\n    let markdownText = '';\n\n    function traverse(node) {\n      if (node.nodeType === Node.TEXT_NODE) {\n        // text node\n        markdownText += node.textContent;\n      } else if (node.nodeType === Node.ELEMENT_NODE) {\n        // element node\n        const tagName = node.tagName.toLowerCase();\n\n        if (tagName === 'img') {\n          // img node\n          const src = node.getAttribute('src');\n          const alt = node.getAttribute('alt') || t('image.text');\n          markdownText += `![${alt}](${src})`;\n        } else if (tagName === 'br') {\n          // br node\n          markdownText += '\\n';\n        } else {\n          for (let i = 0; i < node.childNodes.length; i += 1) {\n            traverse(node.childNodes[i]);\n          }\n        }\n\n        const blockLevelElements = [\n          'p',\n          'div',\n          'h1',\n          'h2',\n          'h3',\n          'h4',\n          'h5',\n          'h6',\n          'ul',\n          'ol',\n          'li',\n          'blockquote',\n          'pre',\n          'table',\n          'thead',\n          'tbody',\n          'tr',\n          'th',\n          'td',\n        ];\n        if (blockLevelElements.includes(tagName)) {\n          markdownText += '\\n\\n';\n        }\n      }\n    }\n\n    traverse(body);\n\n    markdownText = markdownText.replace(/[\\n\\s]+/g, (match) => {\n      return match.length > 1 ? '\\n\\n' : match;\n    });\n\n    if (editorState) {\n      editorState.replaceSelection(markdownText);\n    }\n  };\n  const handleClick = () => {\n    if (!link.value) {\n      setLink({ ...link, isInvalid: true });\n      return;\n    }\n    setLink({ ...link, type: '' });\n\n    if (editorState) {\n      editorState.insertImage(link.value, imageName.value || undefined);\n    }\n\n    setVisible(false);\n\n    editorState?.focus();\n    setLink({ ...link, value: '' });\n    setImageName({ ...imageName, value: '' });\n  };\n  useEffect(() => {\n    if (!editorState) {\n      return undefined;\n    }\n\n    editorState.on('dragenter', dragenter);\n    editorState.on('dragover', dragover);\n    editorState.on('drop', drop);\n    editorState.on('paste', paste);\n\n    return () => {\n      editorState.off('dragenter', dragenter);\n      editorState.off('dragover', dragover);\n      editorState.off('drop', drop);\n      editorState.off('paste', paste);\n    };\n  }, [editorState]);\n\n  useEffect(() => {\n    if (link.value && link.type === 'drop') {\n      handleClick();\n    }\n  }, [link.value]);\n\n  const addLink = (editorInstance: Editor) => {\n    setEditorState(editorInstance);\n    const text = editorInstance?.getSelection();\n\n    setImageName({ ...imageName, value: text });\n\n    setVisible(true);\n  };\n\n  const { uploadSingleFile } = useImageUpload();\n\n  const onUpload = async (e) => {\n    if (!editor) {\n      return;\n    }\n    const files = e.target?.files || [];\n    const bool = verifyImageSize(files);\n\n    if (!bool) {\n      return;\n    }\n\n    uploadSingleFile(e.target.files[0]).then((url) => {\n      setLink({ ...link, value: url });\n      setImageName({ ...imageName, value: files[0].name });\n    });\n  };\n\n  const onHide = () => setVisible(false);\n  const onExited = () => editor?.focus();\n\n  const handleSelect = (tab) => {\n    setCurrentTab(tab);\n  };\n  return (\n    <ToolItem {...item} onClick={addLink}>\n      <Modal\n        show={visible}\n        onHide={onHide}\n        onExited={onExited}\n        fullscreen=\"sm-down\">\n        <Modal.Header closeButton>\n          <h5 className=\"mb-0\">{t('image.add_image')}</h5>\n        </Modal.Header>\n        <Modal.Body>\n          <Tabs onSelect={handleSelect}>\n            <Tab eventKey=\"localImage\" title={t('image.tab_image')}>\n              <Form className=\"mt-3\" onSubmit={handleClick}>\n                <Form.Group controlId=\"editor.imgLink\" className=\"mb-3\">\n                  <Form.Label>\n                    {t('image.form_image.fields.file.label')}\n                  </Form.Label>\n                  <Form.Control\n                    type=\"file\"\n                    onChange={onUpload}\n                    isInvalid={currentTab === 'localImage' && link.isInvalid}\n                    accept=\"image/*\"\n                  />\n\n                  <Form.Control.Feedback type=\"invalid\">\n                    {t('image.form_image.fields.file.msg.empty')}\n                  </Form.Control.Feedback>\n                </Form.Group>\n\n                <Form.Group controlId=\"editor.imgDescription\" className=\"mb-3\">\n                  <Form.Label>\n                    {`${t('image.form_image.fields.desc.label')} ${t(\n                      'optional',\n                      {\n                        keyPrefix: 'form',\n                      },\n                    )}`}\n                  </Form.Label>\n                  <Form.Control\n                    type=\"text\"\n                    value={imageName.value}\n                    onChange={(e) =>\n                      setImageName({ ...imageName, value: e.target.value })\n                    }\n                    isInvalid={imageName.isInvalid}\n                  />\n                </Form.Group>\n              </Form>\n            </Tab>\n            <Tab eventKey=\"remoteImage\" title={t('image.tab_url')}>\n              <Form className=\"mt-3\" onSubmit={handleClick}>\n                <Form.Group controlId=\"editor.imgUrl\" className=\"mb-3\">\n                  <Form.Label>\n                    {t('image.form_url.fields.url.label')}\n                  </Form.Label>\n                  <Form.Control\n                    type=\"text\"\n                    value={link.value}\n                    onChange={(e) =>\n                      setLink({ ...link, value: e.target.value })\n                    }\n                    isInvalid={currentTab === 'remoteImage' && link.isInvalid}\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {t('image.form_url.fields.url.msg.empty')}\n                  </Form.Control.Feedback>\n                </Form.Group>\n\n                <Form.Group controlId=\"editor.imgName\" className=\"mb-3\">\n                  <Form.Label>\n                    {`${t('image.form_url.fields.name.label')} ${t('optional', {\n                      keyPrefix: 'form',\n                    })}`}\n                  </Form.Label>\n                  <Form.Control\n                    type=\"text\"\n                    value={imageName.value}\n                    onChange={(e) =>\n                      setImageName({ ...imageName, value: e.target.value })\n                    }\n                    isInvalid={imageName.isInvalid}\n                  />\n                </Form.Group>\n              </Form>\n            </Tab>\n          </Tabs>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => setVisible(false)}>\n            {t('image.btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleClick}>\n            {t('image.btn_confirm')}\n          </Button>\n        </Modal.Footer>\n      </Modal>\n    </ToolItem>\n  );\n};\n\nexport default memo(Image);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/indent.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Indent = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'text-indent-left',\n    tip: t('indent.text'),\n  };\n  const handleClick = (editor: Editor) => {\n    editor.indent();\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Indent);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Table from './table';\nimport OL from './ol';\nimport UL from './ul';\nimport Indent from './indent';\nimport Outdent from './outdent';\nimport Hr from './hr';\nimport Heading from './heading';\nimport Bold from './bold';\nimport Italice from './italic';\nimport Code from './code';\nimport Link from './link';\nimport BlockQuote from './blockquote';\nimport Image from './image';\nimport Help from './help';\nimport File from './file';\n\nexport {\n  Table,\n  OL,\n  UL,\n  Indent,\n  Outdent,\n  Hr,\n  Heading,\n  Bold,\n  Italice,\n  Code,\n  Link,\n  BlockQuote,\n  Image,\n  Help,\n  File,\n};\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/italic.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Italic = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'type-italic',\n    keyMap: ['Ctrl-i'],\n    tip: `${t('italic.text')} (Ctrl+i)`,\n  };\n  const DEFAULTTEXT = t('italic.text');\n\n  const handleClick = (editor: Editor) => {\n    editor.insertItalic(DEFAULTTEXT);\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Italic);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/link.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useRef, useState, memo } from 'react';\nimport { Button, Form, Modal } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Link = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'link-45deg',\n    keyMap: ['Ctrl-l'],\n    tip: `${t('link.text')} (Ctrl+l)`,\n  };\n  const [visible, setVisible] = useState(false);\n  const [currentEditor, setCurrentEditor] = useState<Editor | null>(null);\n  const [link, setLink] = useState({\n    value: 'https://',\n    isInvalid: false,\n    errorMsg: '',\n  });\n  const [name, setName] = useState({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  });\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (visible && inputRef.current) {\n      inputRef.current.setSelectionRange(0, inputRef.current.value.length);\n      inputRef.current.focus();\n    }\n  }, [visible]);\n\n  const addLink = (editor: Editor) => {\n    setCurrentEditor(editor);\n    const text = editor.getSelection();\n    setName({ ...name, value: text });\n    setVisible(true);\n  };\n  const handleClick = () => {\n    if (!currentEditor) {\n      return;\n    }\n\n    if (!link.value) {\n      setLink({ ...link, isInvalid: true });\n      return;\n    }\n\n    currentEditor.insertLink(link.value, name.value || undefined);\n\n    setVisible(false);\n    currentEditor.focus();\n    setLink({ ...link, value: '' });\n    setName({ ...name, value: '' });\n  };\n  const onHide = () => setVisible(false);\n  const onExited = () => {\n    currentEditor?.focus();\n  };\n\n  return (\n    <>\n      <ToolItem {...item} onClick={addLink} />\n      <Modal\n        show={visible}\n        onHide={onHide}\n        onExited={onExited}\n        fullscreen=\"sm-down\">\n        <Modal.Header closeButton>\n          <h5 className=\"mb-0\">{t('link.add_link')}</h5>\n        </Modal.Header>\n        <Modal.Body>\n          <Form onSubmit={handleClick}>\n            <Form.Group controlId=\"editor.internetSite\" className=\"mb-3\">\n              <Form.Label>{t('link.form.fields.url.label')}</Form.Label>\n              <Form.Control\n                ref={inputRef}\n                type=\"text\"\n                value={link.value}\n                onChange={(e) => setLink({ ...link, value: e.target.value })}\n                isInvalid={link.isInvalid}\n              />\n            </Form.Group>\n\n            <Form.Group controlId=\"editor.internetSiteName\" className=\"mb-3\">\n              <Form.Label>{`${t('link.form.fields.name.label')} ${t(\n                'optional',\n                {\n                  keyPrefix: 'form',\n                },\n              )}`}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={name.value}\n                onChange={(e) => setName({ ...name, value: e.target.value })}\n                isInvalid={name.isInvalid}\n              />\n            </Form.Group>\n          </Form>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => setVisible(false)}>\n            {t('link.btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleClick}>\n            {t('link.btn_confirm')}\n          </Button>\n        </Modal.Footer>\n      </Modal>\n    </>\n  );\n};\n\nexport default memo(Link);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/ol.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst OL = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'list-ol',\n    keyMap: ['Ctrl-o'],\n    tip: `${t('ordered_list.text')} (Ctrl+o)`,\n  };\n\n  const handleClick = (editor: Editor) => {\n    editor.insertOrderedList();\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(OL);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/outdent.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Outdent = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'text-indent-right',\n    keyMap: ['Shift-Tab'],\n    tip: t('outdent.text'),\n  };\n  const handleClick = (editor: Editor) => {\n    editor.outdent();\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Outdent);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/table.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst Table = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'table',\n    tip: t('table.text'),\n  };\n\n  const handleClick = (editor: Editor) => {\n    editor.insertTable(3, 3);\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(Table);\n"
  },
  {
    "path": "ui/src/components/Editor/ToolBars/ul.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ToolItem from '../toolItem';\nimport { Editor } from '../types';\n\nconst UL = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const item = {\n    label: 'list-ul',\n    keyMap: ['Ctrl-u'],\n    tip: `${t('unordered_list.text')} (Ctrl+u)`,\n  };\n\n  const handleClick = (editor: Editor) => {\n    editor.insertUnorderedList();\n    editor.focus();\n  };\n\n  return <ToolItem {...item} onClick={handleClick} />;\n};\n\nexport default memo(UL);\n"
  },
  {
    "path": "ui/src/components/Editor/Viewer.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport {\n  forwardRef,\n  useEffect,\n  useRef,\n  useState,\n  memo,\n  useImperativeHandle,\n} from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { markdownToHtml } from '@/services';\nimport ImgViewer from '@/components/ImgViewer';\n\nimport { htmlRender } from './utils';\n\nlet scrollTop = 0;\nlet renderTimer;\n\nconst Index = ({ value }, ref) => {\n  const [html, setHtml] = useState('');\n  const previewRef = useRef<HTMLDivElement>(null);\n  const { t } = useTranslation('translation', { keyPrefix: 'messages' });\n\n  const renderMarkdown = (markdown) => {\n    clearTimeout(renderTimer);\n    const timeout = renderTimer ? 1000 : 0;\n    renderTimer = setTimeout(() => {\n      markdownToHtml(markdown).then((resp) => {\n        scrollTop = previewRef.current?.scrollTop || 0;\n        setHtml(resp);\n      });\n    }, timeout);\n  };\n  useEffect(() => {\n    renderMarkdown(value);\n  }, [value]);\n\n  useEffect(() => {\n    if (!html) {\n      return;\n    }\n\n    previewRef.current?.scrollTo(0, scrollTop);\n\n    htmlRender(previewRef.current, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [html]);\n\n  useImperativeHandle(ref, () => {\n    return {\n      getHtml: () => html,\n      element: previewRef.current,\n    };\n  });\n\n  return (\n    <ImgViewer>\n      <div\n        ref={previewRef}\n        className=\"preview-wrap position-relative p-3 rounded text-break text-wrap mt-2 fmt\"\n        dangerouslySetInnerHTML={{ __html: html }}\n      />\n    </ImgViewer>\n  );\n};\n\nexport default memo(forwardRef(Index));\n"
  },
  {
    "path": "ui/src/components/Editor/hooks/useImageUpload.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal as AnswerModal } from '@/components';\nimport { uploadImage } from '@/services';\nimport { writeSettingStore } from '@/stores';\n\nexport const useImageUpload = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'editor' });\n  const {\n    max_image_size = 4,\n    max_attachment_size = 8,\n    authorized_image_extensions = [],\n    authorized_attachment_extensions = [],\n  } = writeSettingStore((state) => state.write);\n\n  const verifyImageSize = (files: FileList | File[]): boolean => {\n    const fileArray = Array.isArray(files) ? files : Array.from(files);\n\n    if (fileArray.length === 0) {\n      return false;\n    }\n\n    const canUploadAttachment = authorized_attachment_extensions.length > 0;\n    const allowedAllType = [\n      ...authorized_image_extensions,\n      ...authorized_attachment_extensions,\n    ];\n\n    const unSupportFiles = fileArray.filter((file) => {\n      const fileName = file.name.toLowerCase();\n      return canUploadAttachment\n        ? !allowedAllType.find((v) => fileName.endsWith(v))\n        : file.type.indexOf('image') === -1;\n    });\n\n    if (unSupportFiles.length > 0) {\n      AnswerModal.confirm({\n        content: canUploadAttachment\n          ? t('file.not_supported', { file_type: allowedAllType.join(', ') })\n          : t('image.form_image.fields.file.msg.only_image'),\n        showCancel: false,\n      });\n      return false;\n    }\n\n    const otherFiles = fileArray.filter((file) => {\n      return file.type.indexOf('image') === -1;\n    });\n\n    if (canUploadAttachment && otherFiles.length > 0) {\n      const attachmentOverSizeFiles = otherFiles.filter(\n        (file) => file.size / 1024 / 1024 > max_attachment_size,\n      );\n      if (attachmentOverSizeFiles.length > 0) {\n        AnswerModal.confirm({\n          content: t('file.max_size', { size: max_attachment_size }),\n          showCancel: false,\n        });\n        return false;\n      }\n    }\n\n    const imageFiles = fileArray.filter(\n      (file) => file.type.indexOf('image') > -1,\n    );\n    const oversizedImages = imageFiles.filter(\n      (file) => file.size / 1024 / 1024 > max_image_size,\n    );\n    if (oversizedImages.length > 0) {\n      AnswerModal.confirm({\n        content: t('image.form_image.fields.file.msg.max_size', {\n          size: max_image_size,\n        }),\n        showCancel: false,\n      });\n      return false;\n    }\n\n    return true;\n  };\n\n  const uploadFiles = (\n    files: FileList | File[],\n  ): Promise<{ url: string; name: string; type: string }[]> => {\n    const fileArray = Array.isArray(files) ? files : Array.from(files);\n    const promises = fileArray.map(async (file) => {\n      const type = file.type.indexOf('image') > -1 ? 'post' : 'post_attachment';\n      const url = await uploadImage({ file, type });\n\n      return {\n        name: file.name,\n        url,\n        type,\n      };\n    });\n\n    return Promise.all(promises);\n  };\n\n  const uploadSingleFile = async (file: File): Promise<string> => {\n    const type = file.type.indexOf('image') > -1 ? 'post' : 'post_attachment';\n    return uploadImage({ file, type });\n  };\n\n  return {\n    verifyImageSize,\n    uploadFiles,\n    uploadSingleFile,\n  };\n};\n"
  },
  {
    "path": "ui/src/components/Editor/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.md-editor-wrap {\n  display: flex;\n  flex-direction: column;\n  background-color: var(-bs-body-bg);\n  border: 1px solid var(--an-ced4da);\n  overflow: hidden;\n  .toolbar-wrap {\n    border-bottom: 1px solid var(--an-ced4da);\n    .toolbar-divider {\n      float: left;\n      width: 1px;\n      height: 15px;\n      background-color: var(--an-toolbar-divider);\n      margin: 10px 8px;\n    }\n    .toolbar-item-wrap {\n      position: relative;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      height: 24px;\n      width: 24px;\n      margin: 0 2px;\n      line-height: 0;\n      .dropdown-menu {\n        line-height: 1.5;\n      }\n      &.right {\n        float: right;\n      }\n      .dropdown-toggle {\n        &::after {\n          display: none;\n        }\n      }\n    }\n    .toolbar {\n      box-sizing: border-box;\n      outline: none;\n      cursor: pointer;\n      background-color: var(--bs-body-bg);\n      height: 100%;\n      width: 100%;\n      border-radius: 3px;\n      font-size: 20px;\n      line-height: 20px;\n      &:hover {\n        background-color: var(--an-editor-toolbar-hover);\n      }\n      &:focus {\n        background-color: var(--ans-editor-toolbar-focus);\n      }\n    }\n    .popup-wrap {\n      position: absolute;\n      width: 500px;\n      margin-right: auto;\n      border: 1px solid #cacaca;\n      background: #fff;\n      z-index: 9999;\n      visibility: hidden;\n\n      &.heading-add {\n        width: 158px;\n        padding: 8px 0;\n\n        ul {\n          list-style: none;\n          padding: 0;\n          margin: 0;\n          li {\n            padding: 4px 24px;\n            cursor: pointer;\n            &:hover {\n              background-color: #eee;\n            }\n            h2 {\n              font-size: 24px;\n              margin: 0;\n            }\n            h3 {\n              font-size: 20px;\n              margin: 0;\n            }\n            h4 {\n              font-size: 19px;\n              margin: 0;\n            }\n          }\n        }\n      }\n    }\n  }\n  .content-wrap {\n    height: 264px;\n  }\n\n  .rich-editor-wrap {\n    height: 264px;\n    overflow-y: auto;\n    padding: 0.375rem 0.75rem;\n\n    .tiptap-editor {\n      outline: none;\n      min-height: 100%;\n\n      &:focus {\n        outline: none;\n      }\n\n      p {\n        margin: 0.5rem 0;\n        line-height: 1.6;\n\n        &.is-editor-empty:first-child::before {\n          content: attr(data-placeholder);\n          float: left;\n          color: var(--an-editor-placeholder-color);\n          pointer-events: none;\n          height: 0;\n        }\n      }\n    }\n\n    .editor-loading {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      height: 100%;\n      color: var(--an-text-secondary, #6c757d);\n    }\n  }\n\n  .CodeMirror {\n    height: auto;\n    font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',\n      'Courier New', monospace !important;\n    font-size: 14px;\n    pre.CodeMirror-line,\n    pre.CodeMirror-line-like {\n      padding: 0 16px;\n    }\n    .CodeMirror-lines {\n      padding: 16px 0;\n    }\n    .CodeMirror-placeholder {\n      color: var(--an-editor-placeholder-color);\n    }\n  }\n}\n.preview-wrap {\n  overflow-y: auto;\n  min-height: 20px;\n  padding: 16px;\n  img {\n    max-width: 100%;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/Editor/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport {\n  useRef,\n  useState,\n  ForwardRefRenderFunction,\n  forwardRef,\n  useImperativeHandle,\n  useCallback,\n  useEffect,\n} from 'react';\nimport { Spinner } from 'react-bootstrap';\n\nimport classNames from 'classnames';\n\nimport {\n  PluginType,\n  useRenderPlugin,\n  getReplacementPlugin,\n} from '@/utils/pluginKit';\nimport { writeSettingStore } from '@/stores';\nimport PluginRender, { PluginSlot } from '../PluginRender';\n\nimport { useImageUpload } from './hooks/useImageUpload';\nimport {\n  BlockQuote,\n  Bold,\n  Code,\n  Heading,\n  Help,\n  Hr,\n  Image,\n  Indent,\n  Italice,\n  Link as LinkItem,\n  OL,\n  Outdent,\n  Table,\n  UL,\n  File,\n} from './ToolBars';\nimport { htmlRender } from './utils';\nimport Viewer from './Viewer';\nimport { EditorContext } from './EditorContext';\nimport MarkdownEditor from './MarkdownEditor';\nimport { Editor } from './types';\n\nimport './index.scss';\n\nexport interface EditorRef {\n  getHtml: () => string;\n}\n\ninterface EventRef {\n  onChange?(value: string): void;\n  onFocus?(): void;\n  onBlur?(): void;\n}\n\ninterface Props extends EventRef {\n  editorPlaceholder?;\n  className?;\n  value;\n  autoFocus?: boolean;\n}\n\nconst MDEditor: ForwardRefRenderFunction<EditorRef, Props> = (\n  {\n    editorPlaceholder = '',\n    className = '',\n    value,\n    onChange,\n    onFocus,\n    onBlur,\n    autoFocus = false,\n  },\n  ref,\n) => {\n  const [currentEditor, setCurrentEditor] = useState<Editor | null>(null);\n  const previewRef = useRef<{ getHtml; element } | null>(null);\n  const [fullEditorPlugin, setFullEditorPlugin] = useState<any>(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const { verifyImageSize, uploadSingleFile } = useImageUpload();\n  const {\n    max_image_size = 4,\n    authorized_image_extensions = [],\n    authorized_attachment_extensions = [],\n  } = writeSettingStore((state) => state.write);\n\n  useEffect(() => {\n    let mounted = true;\n\n    const loadPlugin = async () => {\n      const plugin = await getReplacementPlugin(PluginType.EditorReplacement);\n      if (mounted) {\n        setFullEditorPlugin(plugin);\n        setIsLoading(false);\n      }\n    };\n\n    loadPlugin();\n\n    return () => {\n      mounted = false;\n    };\n  }, []);\n\n  useRenderPlugin(previewRef.current?.element);\n\n  const getHtml = useCallback(() => {\n    return previewRef.current?.getHtml();\n  }, []);\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      getHtml,\n    }),\n    [getHtml],\n  );\n\n  const EditorComponent = MarkdownEditor;\n\n  if (isLoading) {\n    return (\n      <div className={classNames('md-editor-wrap rounded', className)}>\n        <div\n          className=\"d-flex justify-content-center align-items-center\"\n          style={{ minHeight: '200px' }}>\n          <Spinner animation=\"border\" variant=\"secondary\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (fullEditorPlugin) {\n    const FullEditorComponent = fullEditorPlugin.component;\n\n    const handleImageUpload = async (file: File | string): Promise<string> => {\n      if (typeof file === 'string') {\n        return file;\n      }\n\n      if (!verifyImageSize([file])) {\n        throw new Error('File validation failed');\n      }\n\n      return uploadSingleFile(file);\n    };\n\n    return (\n      <FullEditorComponent\n        value={value}\n        onChange={onChange}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        placeholder={editorPlaceholder}\n        autoFocus={autoFocus}\n        imageUploadHandler={handleImageUpload}\n        uploadConfig={{\n          maxImageSizeMiB: max_image_size,\n          allowedExtensions: [\n            ...authorized_image_extensions,\n            ...authorized_attachment_extensions,\n          ],\n        }}\n      />\n    );\n  }\n\n  return (\n    <>\n      <div className={classNames('md-editor-wrap rounded', className)}>\n        <div className=\"toolbar-wrap px-3 d-flex align-items-center flex-wrap\">\n          <EditorContext.Provider value={currentEditor}>\n            <PluginRender\n              type={PluginType.Editor}\n              className=\"d-flex align-items-center flex-wrap\"\n              editor={currentEditor}\n              previewElement={previewRef.current?.element}>\n              <Heading />\n              <Bold />\n              <Italice />\n              <div className=\"toolbar-divider\" />\n              <Code />\n              <LinkItem />\n              <BlockQuote />\n              <Image />\n              <File />\n              <Table />\n              <div className=\"toolbar-divider\" />\n              <OL />\n              <UL />\n              <Indent />\n              <Outdent />\n              <Hr />\n              <div className=\"toolbar-divider\" />\n              <PluginSlot />\n              <Help />\n            </PluginRender>\n          </EditorContext.Provider>\n        </div>\n\n        <EditorComponent\n          key=\"markdown-editor\"\n          value={value}\n          onChange={(markdown) => {\n            onChange?.(markdown);\n          }}\n          onFocus={onFocus}\n          onBlur={onBlur}\n          placeholder={editorPlaceholder}\n          autoFocus={autoFocus}\n          onEditorReady={(editor) => {\n            setCurrentEditor(editor);\n          }}\n        />\n      </div>\n      <Viewer ref={previewRef} value={value} />\n    </>\n  );\n};\nexport { htmlRender };\nexport default forwardRef(MDEditor);\n"
  },
  {
    "path": "ui/src/components/Editor/toolItem.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useContext, useEffect } from 'react';\nimport { Dropdown, Button } from 'react-bootstrap';\n\nimport { EditorContext } from './EditorContext';\nimport { Editor } from './types';\n\ninterface IProps {\n  keyMap?: string[];\n  onClick?: (editor: Editor) => void;\n  tip?: string;\n  className?: string;\n  as?: any;\n  children?;\n  label?: string;\n  disable?: boolean;\n  isShow?: boolean;\n  onBlur?: (editor: Editor) => void;\n}\nconst ToolItem: FC<IProps> = (props) => {\n  const editor = useContext(EditorContext);\n\n  const {\n    label,\n    tip,\n    disable = false,\n    isShow,\n    keyMap,\n    onClick,\n    className,\n    as,\n    children,\n    onBlur,\n  } = props;\n\n  useEffect(() => {\n    if (!editor) {\n      return;\n    }\n    if (!keyMap) {\n      return;\n    }\n\n    keyMap.forEach((key) => {\n      editor?.addKeyMap({\n        [key]: () => {\n          onClick?.(editor);\n          return true;\n        },\n      });\n    });\n  }, [editor]);\n\n  const btnRender = () => (\n    <Button\n      variant=\"link\"\n      title={tip}\n      className={`p-0 b-0 btn-no-border toolbar text-body ${\n        disable ? 'disabled' : ''\n      }`}\n      disabled={disable}\n      tabIndex={-1}\n      onClick={(e) => {\n        e.preventDefault();\n        if (editor) {\n          onClick?.(editor);\n        }\n      }}\n      onBlur={(e) => {\n        e.preventDefault();\n        if (editor) {\n          onBlur?.(editor);\n        }\n      }}>\n      <i className={`bi bi-${label}`} />\n    </Button>\n  );\n\n  if (!editor) {\n    return null;\n  }\n  return (\n    <div className={`toolbar-item-wrap ${className || ''}`}>\n      {as === 'dropdown' ? (\n        <Dropdown className=\"h-100 w-100\" show={isShow}>\n          <Dropdown.Toggle as=\"div\" className=\"h-100\">\n            {btnRender()}\n          </Dropdown.Toggle>\n          {children}\n        </Dropdown>\n      ) : (\n        <>\n          {btnRender()}\n          {children}\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default ToolItem;\n"
  },
  {
    "path": "ui/src/components/Editor/types.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { EditorView, Command } from '@codemirror/view';\n\nexport interface Position {\n  ch: number;\n  line: number;\n  sticky?: string | undefined;\n}\n\nexport type Level = 1 | 2 | 3 | 4 | 5 | 6;\n\nexport interface ExtendEditor {\n  addKeyMap: (keyMap: Record<string, Command>) => void;\n  on: (\n    event:\n      | 'change'\n      | 'focus'\n      | 'blur'\n      | 'dragenter'\n      | 'dragover'\n      | 'drop'\n      | 'paste',\n    callback: (e?) => void,\n  ) => void;\n  getValue: () => string;\n  setValue: (value: string) => void;\n  off: (\n    event:\n      | 'change'\n      | 'focus'\n      | 'blur'\n      | 'dragenter'\n      | 'dragover'\n      | 'drop'\n      | 'paste',\n    callback: (e?) => void,\n  ) => void;\n  getSelection: () => string;\n  replaceSelection: (value: string) => void;\n  focus: () => void;\n  getCursor: () => Position;\n  replaceRange: (value: string, from: Position, to: Position) => void;\n  setSelection: (anchor: Position, head?: Position) => void;\n  setReadOnly: (readOnly: boolean) => void;\n\n  wrapText: (before: string, after?: string, defaultText?: string) => void;\n  replaceLines: (\n    replace: Parameters<Array<string>['map']>[0],\n    symbolLen?: number,\n  ) => void;\n  appendBlock: (content: string) => void;\n\n  insertBold: (text?: string) => void;\n  insertItalic: (text?: string) => void;\n  insertCode: (text?: string) => void;\n  insertStrikethrough: (text?: string) => void;\n\n  insertHeading: (level: Level, text?: string) => void;\n  insertBlockquote: (text?: string) => void;\n  insertCodeBlock: (language?: string, code?: string) => void;\n  insertHorizontalRule: () => void;\n\n  insertOrderedList: () => void;\n  insertUnorderedList: () => void;\n  toggleOrderedList: () => void;\n  toggleUnorderedList: () => void;\n\n  insertLink: (url: string, text?: string) => void;\n  insertImage: (url: string, alt?: string) => void;\n\n  insertTable: (rows?: number, cols?: number) => void;\n\n  indent: () => void;\n  outdent: () => void;\n\n  isBold: () => boolean;\n  isItalic: () => boolean;\n  isHeading: (level?: number) => boolean;\n  isBlockquote: () => boolean;\n  isCodeBlock: () => boolean;\n  isOrderedList: () => boolean;\n  isUnorderedList: () => boolean;\n}\n\nexport type Editor = EditorView & ExtendEditor;\nexport interface CodeMirrorEditor extends Editor {\n  display: any;\n\n  moduleType;\n}\n\nexport interface BaseEditorProps {\n  value: string;\n  onChange?: (value: string) => void;\n  onFocus?: () => void;\n  onBlur?: () => void;\n  placeholder?: string;\n  autoFocus?: boolean;\n  onEditorReady?: (editor: Editor) => void;\n}\n"
  },
  {
    "path": "ui/src/components/Editor/utils/codemirror/adapter.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Editor, ExtendEditor } from '../../types';\n\nimport { createBaseMethods } from './base';\nimport { createEventMethods } from './events';\nimport { createCommandMethods } from './commands';\n\n/**\n * Adapts CodeMirror editor to unified editor interface\n *\n * This adapter function extends CodeMirror editor with additional methods,\n * enabling toolbar components to work properly in Markdown mode. The adapter\n * implements the complete `ExtendEditor` interface, including base methods,\n * event handling, and command methods.\n *\n * @param editor - CodeMirror editor instance\n * @returns Extended editor instance that implements the unified Editor interface\n *\n * @example\n * ```typescript\n * const cmEditor = new EditorView({ ... });\n * const adaptedEditor = createCodeMirrorAdapter(cmEditor as Editor);\n * // Now you can use the unified API\n * adaptedEditor.insertBold('text');\n * adaptedEditor.insertHeading(1, 'Title');\n * ```\n */\nexport function createCodeMirrorAdapter(editor: Editor): Editor {\n  const baseMethods = createBaseMethods(editor);\n  const eventMethods = createEventMethods(editor);\n  const commandMethods = createCommandMethods(editor);\n\n  const editorAdapter: ExtendEditor = {\n    ...editor,\n    ...baseMethods,\n    ...eventMethods,\n    ...commandMethods,\n  };\n\n  return editorAdapter as unknown as Editor;\n}\n"
  },
  {
    "path": "ui/src/components/Editor/utils/codemirror/base.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { EditorSelection, StateEffect } from '@codemirror/state';\nimport { keymap, KeyBinding, Command } from '@codemirror/view';\n\nimport { Editor, Position } from '../../types';\n\n/**\n * Creates base methods module\n *\n * Provides core base methods for the editor, including:\n * - Content getter and setter (getValue, setValue)\n * - Selection operations (getSelection, replaceSelection)\n * - Cursor and selection position (getCursor, setSelection)\n * - Focus and keyboard mapping (focus, addKeyMap)\n *\n * @param editor - CodeMirror editor instance\n * @returns Object containing base methods\n */\nexport function createBaseMethods(editor: Editor) {\n  return {\n    focus: () => {\n      editor.contentDOM.focus();\n    },\n\n    getCursor: () => {\n      const range = editor.state.selection.ranges[0];\n      const line = editor.state.doc.lineAt(range.from).number;\n      const { from, to } = editor.state.doc.line(line);\n      return { from, to, ch: range.from - from, line };\n    },\n\n    addKeyMap: (keyMap: Record<string, Command>) => {\n      const array = Object.entries(keyMap).map(([key, value]) => {\n        const keyBinding: KeyBinding = {\n          key,\n          preventDefault: true,\n          run: value,\n        };\n        return keyBinding;\n      });\n\n      editor.dispatch({\n        effects: StateEffect.appendConfig.of(keymap.of(array)),\n      });\n    },\n\n    getSelection: () => {\n      return editor.state.sliceDoc(\n        editor.state.selection.main.from,\n        editor.state.selection.main.to,\n      );\n    },\n\n    replaceSelection: (value: string) => {\n      editor.dispatch({\n        changes: [\n          {\n            from: editor.state.selection.main.from,\n            to: editor.state.selection.main.to,\n            insert: value,\n          },\n        ],\n        selection: EditorSelection.cursor(\n          editor.state.selection.main.from + value.length,\n        ),\n      });\n    },\n\n    setSelection: (anchor: Position, head?: Position) => {\n      editor.dispatch({\n        selection: EditorSelection.create([\n          EditorSelection.range(\n            editor.state.doc.line(anchor.line).from + anchor.ch,\n            head\n              ? editor.state.doc.line(head.line).from + head.ch\n              : editor.state.doc.line(anchor.line).from + anchor.ch,\n          ),\n        ]),\n      });\n    },\n\n    getValue: () => {\n      return editor.state.doc.toString();\n    },\n\n    setValue: (value: string) => {\n      editor.dispatch({\n        changes: { from: 0, to: editor.state.doc.length, insert: value },\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "ui/src/components/Editor/utils/codemirror/commands.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { EditorSelection } from '@codemirror/state';\n\nimport { Editor, Level } from '../../types';\n\n/**\n * Creates command methods module\n *\n * Provides semantic command methods and low-level text manipulation methods:\n * - Semantic methods: insertBold, insertHeading, insertImage, etc. (for toolbar use)\n * - Low-level methods: wrapText, replaceLines, appendBlock (for internal use)\n * - State query methods: isBold, isHeading, etc.\n *\n * @param editor - CodeMirror editor instance\n * @returns Object containing all command methods\n */\nexport function createCommandMethods(editor: Editor) {\n  // Create methods object that allows self-reference\n  const methods = {\n    wrapText: (before: string, after = before, defaultText) => {\n      const range = editor.state.selection.ranges[0];\n      const selectedText = editor.state.sliceDoc(range.from, range.to);\n      const text = selectedText || defaultText || '';\n      const wrappedText = before + text + after;\n      const insertFrom = range.from;\n      const insertTo = range.to;\n\n      editor.dispatch({\n        changes: [\n          {\n            from: insertFrom,\n            to: insertTo,\n            insert: wrappedText,\n          },\n        ],\n        selection: selectedText\n          ? EditorSelection.cursor(insertFrom + before.length + text.length)\n          : EditorSelection.range(\n              insertFrom + before.length,\n              insertFrom + before.length + text.length,\n            ),\n      });\n    },\n\n    replaceLines: (replace: Parameters<Array<string>['map']>[0]) => {\n      const { doc } = editor.state;\n      const lines: string[] = [];\n      for (let i = 1; i <= doc.lines; i += 1) {\n        lines.push(doc.line(i).text);\n      }\n\n      const newLines = lines.map(replace) as string[];\n      const newText = newLines.join('\\n');\n      editor.dispatch({\n        changes: {\n          from: 0,\n          to: editor.state.doc.length,\n          insert: newText,\n        },\n      });\n    },\n\n    appendBlock: (content: string) => {\n      const { doc } = editor.state;\n      const currentText = doc.toString();\n      const newText = currentText ? `${currentText}\\n\\n${content}` : content;\n      editor.dispatch({\n        changes: {\n          from: 0,\n          to: editor.state.doc.length,\n          insert: newText,\n        },\n      });\n    },\n\n    insertBold: (text?: string) => {\n      methods.wrapText('**', '**', text || 'bold text');\n    },\n\n    insertItalic: (text?: string) => {\n      methods.wrapText('*', '*', text || 'italic text');\n    },\n\n    insertCode: (text?: string) => {\n      methods.wrapText('`', '`', text || 'code');\n    },\n\n    insertStrikethrough: (text?: string) => {\n      methods.wrapText('~~', '~~', text || 'strikethrough text');\n    },\n\n    insertHeading: (level: Level, text?: string) => {\n      const headingText = '#'.repeat(level);\n      methods.wrapText(`${headingText} `, '', text || 'heading');\n    },\n\n    insertBlockquote: (text?: string) => {\n      methods.wrapText('> ', '', text || 'quote');\n    },\n\n    insertCodeBlock: (language?: string, code?: string) => {\n      const lang = language || '';\n      const codeText = code || '';\n      const block = `\\`\\`\\`${lang}\\n${codeText}\\n\\`\\`\\``;\n      methods.appendBlock(block);\n    },\n\n    insertHorizontalRule: () => {\n      methods.appendBlock('---');\n    },\n\n    insertOrderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      if (/^\\d+\\.\\s/.test(lineText)) {\n        return;\n      }\n      methods.replaceLines((lineItem) => {\n        if (lineItem.trim() === '') {\n          return lineItem;\n        }\n        return `1. ${lineItem}`;\n      });\n    },\n\n    insertUnorderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      if (/^[-*+]\\s/.test(lineText)) {\n        return;\n      }\n      methods.replaceLines((lineItem) => {\n        if (lineItem.trim() === '') {\n          return lineItem;\n        }\n        return `- ${lineItem}`;\n      });\n    },\n\n    toggleOrderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      if (/^\\d+\\.\\s/.test(lineText)) {\n        methods.replaceLines((lineItem) => {\n          return lineItem.replace(/^\\d+\\.\\s/, '');\n        });\n      } else {\n        methods.insertOrderedList();\n      }\n    },\n\n    toggleUnorderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      if (/^[-*+]\\s/.test(lineText)) {\n        methods.replaceLines((lineItem) => {\n          return lineItem.replace(/^[-*+]\\s/, '');\n        });\n      } else {\n        methods.insertUnorderedList();\n      }\n    },\n\n    insertLink: (url: string, text?: string) => {\n      const linkText = text || url;\n      methods.wrapText('[', `](${url})`, linkText);\n    },\n\n    insertImage: (url: string, alt?: string) => {\n      const altText = alt || '';\n      methods.wrapText('![', `](${url})`, altText);\n    },\n\n    insertTable: (rows = 3, cols = 3) => {\n      const table: string[] = [];\n      for (let i = 0; i < rows; i += 1) {\n        const row: string[] = [];\n        for (let j = 0; j < cols; j += 1) {\n          row.push(i === 0 ? 'Header' : 'Cell');\n        }\n        table.push(`| ${row.join(' | ')} |`);\n        if (i === 0) {\n          table.push(`| ${'---'.repeat(cols).split('').join(' | ')} |`);\n        }\n      }\n      methods.appendBlock(table.join('\\n'));\n    },\n\n    indent: () => {\n      methods.replaceLines((line) => {\n        if (line.trim() === '') {\n          return line;\n        }\n        return `  ${line}`;\n      });\n    },\n\n    outdent: () => {\n      methods.replaceLines((line) => {\n        if (line.trim() === '') {\n          return line;\n        }\n        return line.replace(/^ {2}/, '');\n      });\n    },\n\n    isBold: () => {\n      const selection = editor.getSelection();\n      return /^\\*\\*.*\\*\\*$/.test(selection) || /^__.*__$/.test(selection);\n    },\n\n    isItalic: () => {\n      const selection = editor.getSelection();\n      return /^\\*.*\\*$/.test(selection) || /^_.*_$/.test(selection);\n    },\n\n    isHeading: (level?: number) => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      if (level) {\n        return new RegExp(`^#{${level}}\\\\s`).test(lineText);\n      }\n      return /^#{1,6}\\s/.test(lineText);\n    },\n\n    isBlockquote: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      return /^>\\s/.test(lineText);\n    },\n\n    isCodeBlock: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      return /^```/.test(lineText);\n    },\n\n    isOrderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      return /^\\d+\\.\\s/.test(lineText);\n    },\n\n    isUnorderedList: () => {\n      const cursor = editor.getCursor();\n      const line = editor.state.doc.line(cursor.line);\n      const lineText = line.text.trim();\n      return /^[-*+]\\s/.test(lineText);\n    },\n  };\n\n  return methods;\n}\n"
  },
  {
    "path": "ui/src/components/Editor/utils/codemirror/events.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { StateEffect } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport { Editor } from '../../types';\n\n/**\n * Creates event methods module\n *\n * Provides event listener registration and removal for the editor.\n * Handles various DOM events including focus, blur, drag, drop, and paste.\n *\n * @param editor - CodeMirror editor instance\n * @returns Object containing event methods (on, off)\n */\nexport function createEventMethods(editor: Editor) {\n  return {\n    on: (event, callback) => {\n      if (event === 'change') {\n        const change = EditorView.updateListener.of((update) => {\n          if (update.docChanged) {\n            callback();\n          }\n        });\n\n        editor.dispatch({\n          effects: StateEffect.appendConfig.of(change),\n        });\n      }\n      if (event === 'focus') {\n        editor.contentDOM.addEventListener('focus', callback);\n      }\n      if (event === 'blur') {\n        editor.contentDOM.addEventListener('blur', callback);\n      }\n\n      if (event === 'dragenter') {\n        editor.contentDOM.addEventListener('dragenter', callback);\n      }\n\n      if (event === 'dragover') {\n        editor.contentDOM.addEventListener('dragover', callback);\n      }\n\n      if (event === 'drop') {\n        editor.contentDOM.addEventListener('drop', callback);\n      }\n\n      if (event === 'paste') {\n        editor.contentDOM.addEventListener('paste', callback);\n      }\n    },\n\n    off: (event, callback) => {\n      if (event === 'focus') {\n        editor.contentDOM.removeEventListener('focus', callback);\n      }\n\n      if (event === 'blur') {\n        editor.contentDOM.removeEventListener('blur', callback);\n      }\n\n      if (event === 'dragenter') {\n        editor.contentDOM.removeEventListener('dragenter', callback);\n      }\n\n      if (event === 'dragover') {\n        editor.contentDOM.removeEventListener('dragover', callback);\n      }\n\n      if (event === 'drop') {\n        editor.contentDOM.removeEventListener('drop', callback);\n      }\n\n      if (event === 'paste') {\n        editor.contentDOM.removeEventListener('paste', callback);\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "ui/src/components/Editor/utils/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState, useRef } from 'react';\n\nimport { minimalSetup } from 'codemirror';\nimport { EditorState, Compartment } from '@codemirror/state';\nimport { EditorView, placeholder } from '@codemirror/view';\nimport { markdown, markdownLanguage } from '@codemirror/lang-markdown';\nimport { languages } from '@codemirror/language-data';\nimport copy from 'copy-to-clipboard';\nimport Tooltip from 'bootstrap/js/dist/tooltip';\n\nimport { Editor } from '../types';\nimport { isDarkTheme } from '@/utils/common';\n\nimport { createCodeMirrorAdapter } from './codemirror/adapter';\n\nconst editableCompartment = new Compartment();\ninterface htmlRenderConfig {\n  copyText: string;\n  copySuccessText: string;\n}\nexport function htmlRender(el: HTMLElement | null, config?: htmlRenderConfig) {\n  if (!el) return;\n  const { copyText = '', copySuccessText = '' } = config || {\n    copyText: 'Copy to clipboard',\n    copySuccessText: 'Copied!',\n  };\n  // Replace all br tags with newlines\n  // Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors.\n  el.querySelectorAll('p').forEach((p) => {\n    if (p.innerHTML.startsWith('$$') && p.innerHTML.endsWith('$$')) {\n      const str = p.innerHTML.replace(/<br>/g, '\\n');\n      p.innerHTML = str;\n    }\n  });\n\n  // change table style\n\n  el.querySelectorAll('table').forEach((table) => {\n    if (\n      (table.parentNode as HTMLDivElement)?.classList.contains(\n        'table-responsive',\n      )\n    ) {\n      return;\n    }\n\n    table.classList.add('table', 'table-bordered');\n    const div = document.createElement('div');\n    div.className = 'table-responsive';\n    table.parentNode?.replaceChild(div, table);\n    div.appendChild(table);\n  });\n\n  // add rel nofollow for link not includes domain\n  el.querySelectorAll('a').forEach((a) => {\n    const base = window.location.origin;\n    const targetUrl = new URL(a.href, base);\n\n    if (targetUrl.origin !== base) {\n      a.rel = 'nofollow';\n    }\n  });\n\n  // Add copy button to all pre tags\n  el.querySelectorAll('pre').forEach((pre) => {\n    // Create copy button\n    const codeWrap = document.createElement('div');\n    codeWrap.className = 'position-relative a-code-wrap';\n    const codeTool = document.createElement('div');\n    codeTool.className = 'a-code-tool';\n    const uniqueId = `a-copy-code-${Date.now().toString().substring(5)}-${Math.floor(Math.random() * 10)}${Math.floor(Math.random() * 10)}${Math.floor(Math.random() * 10)}`;\n    const str = `\n      <a role=\"button\" class=\"link-secondary a-copy-code\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" data-bs-title=\"${copyText}\" id=\"${uniqueId}\">\n        <i class=\"br bi-copy\"></i>\n      </a>\n    `;\n    codeTool.innerHTML = str;\n\n    pre.style.position = 'relative';\n\n    codeWrap.appendChild(codeTool);\n    pre.parentNode?.replaceChild(codeWrap, pre);\n    codeWrap.appendChild(pre);\n\n    const tooltipTriggerList = el.querySelectorAll('.a-copy-code');\n\n    Array.from(tooltipTriggerList)?.map(\n      (tooltipTriggerEl) => new Tooltip(tooltipTriggerEl),\n    );\n\n    // Copy pre content on button click\n    const copyBtn = codeTool.querySelector('.a-copy-code');\n    copyBtn?.addEventListener('click', () => {\n      const textToCopy = pre.textContent || '';\n      copy(textToCopy);\n      // Change tooltip text on copy success\n      const tooltipInstance = Tooltip.getOrCreateInstance(`#${uniqueId}`);\n      tooltipInstance?.setContent({ '.tooltip-inner': copySuccessText });\n      const myTooltipEl = document.querySelector(`#${uniqueId}`);\n      myTooltipEl?.addEventListener('hidden.bs.tooltip', () => {\n        tooltipInstance.setContent({ '.tooltip-inner': copyText });\n      });\n    });\n  });\n}\n\nexport const useEditor = ({\n  editorRef,\n  placeholder: placeholderText,\n  autoFocus,\n  initialValue,\n  onChange,\n  onFocus,\n  onBlur,\n}) => {\n  const [editor, setEditor] = useState<Editor | null>(null);\n  const isInternalUpdateRef = useRef<boolean>(false);\n\n  const init = async () => {\n    const isDark = isDarkTheme();\n\n    const theme = EditorView.theme({\n      '&': {\n        height: '100%',\n        padding: '.375rem .75rem',\n      },\n      '&.cm-focused': {\n        outline: 'none',\n      },\n      '.cm-content': {\n        width: '100%',\n      },\n      '.cm-line': {\n        whiteSpace: 'pre-wrap',\n        wordWrap: 'break-word',\n      },\n      '.ͼ7, .ͼ6': {\n        textDecoration: 'none',\n      },\n      '.cm-cursor': {\n        'border-left-color': isDark ? 'white' : 'black',\n      },\n    });\n\n    const startState = EditorState.create({\n      doc: initialValue || '',\n      extensions: [\n        minimalSetup,\n        markdown({\n          codeLanguages: languages,\n          base: markdownLanguage,\n        }),\n        theme,\n        placeholder(placeholderText),\n        EditorView.lineWrapping,\n        editableCompartment.of(EditorView.editable.of(true)),\n        EditorView.domEventHandlers({\n          paste(event) {\n            const clipboard = event.clipboardData as DataTransfer;\n            const htmlStr = clipboard.getData('text/html');\n            const imgRegex =\n              /<img([\\s\\S]*?) src\\s*=\\s*(['\"])([\\s\\S]*?)\\2([^>]*)>/;\n\n            return Boolean(htmlStr.match(imgRegex));\n          },\n        }),\n      ],\n    });\n\n    const view = new EditorView({\n      parent: editorRef.current,\n      state: startState,\n    });\n\n    const cm = createCodeMirrorAdapter(view as Editor);\n\n    cm.setReadOnly = (readOnly: boolean) => {\n      cm.dispatch({\n        effects: editableCompartment.reconfigure(\n          EditorView.editable.of(!readOnly),\n        ),\n      });\n    };\n\n    if (autoFocus) {\n      setTimeout(() => {\n        cm.focus();\n      }, 10);\n    }\n\n    const originalSetValue = cm.setValue;\n    cm.setValue = (newValue: string) => {\n      isInternalUpdateRef.current = true;\n      originalSetValue.call(cm, newValue);\n      setTimeout(() => {\n        isInternalUpdateRef.current = false;\n      }, 0);\n    };\n\n    cm.on('change', () => {\n      if (!isInternalUpdateRef.current && onChange) {\n        const newValue = cm.getValue();\n        onChange(newValue);\n      }\n    });\n\n    cm.on('focus', () => {\n      onFocus?.();\n    });\n\n    cm.on('blur', () => {\n      onBlur?.();\n    });\n\n    setEditor(cm);\n\n    return cm;\n  };\n\n  useEffect(() => {\n    if (!editorRef.current) {\n      return;\n    }\n    if (editorRef.current.children.length > 0 || editor) {\n      return;\n    }\n\n    init();\n  }, [editor]);\n  return editor;\n};\n"
  },
  {
    "path": "ui/src/components/Empty/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, ReactNode } from 'react';\nimport { Trans } from 'react-i18next';\n\nconst Index: FC<{ children?: ReactNode }> = ({ children }) => {\n  return (\n    <div className=\"text-center py-5\">\n      {children || (\n        <Trans i18nKey=\"personal.list_empty\">\n          We couldn't find anything. <br /> Try different or less specific\n          keywords.\n        </Trans>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/FollowingTags/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useState } from 'react';\nimport { Card, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { TagSelector, Tag } from '@/components';\nimport { tryLoggedAndActivated } from '@/utils/guard';\nimport { useFollowingTags, followTags } from '@/services';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  const [isEdit, setEditState] = useState(false);\n  const { data: followingTags, mutate } = useFollowingTags();\n\n  const newTags: any = followingTags?.map((item) => {\n    if (item.slug_name) {\n      return item.slug_name;\n    }\n    return item;\n  });\n\n  const handleFollowTags = () => {\n    followTags({\n      slug_name_list: newTags,\n    });\n    setEditState(false);\n  };\n\n  const handleTagsChange = (value) => {\n    mutate([...value], {\n      revalidate: false,\n    });\n  };\n\n  if (!tryLoggedAndActivated().ok) {\n    return null;\n  }\n  return isEdit ? (\n    <Card className=\"mb-4\">\n      <Card.Header className=\"text-nowrap d-flex justify-content-between\">\n        {t('following_tags')}\n        <Button\n          variant=\"link\"\n          className=\"p-0 m-0 btn-no-border\"\n          onClick={handleFollowTags}>\n          {t('save')}\n        </Button>\n      </Card.Header>\n      <Card.Body>\n        <TagSelector\n          value={followingTags}\n          onChange={handleTagsChange}\n          hiddenDescription\n          hiddenCreateBtn\n          autoFocus\n        />\n      </Card.Body>\n    </Card>\n  ) : (\n    <Card className=\"mb-4\">\n      <Card.Header className=\"text-nowrap d-flex justify-content-between text-capitalize\">\n        {t('following_tags')}\n        <Button\n          variant=\"link\"\n          className=\"p-0 btn-no-border text-capitalize\"\n          onClick={() => setEditState(true)}>\n          {t('edit')}\n        </Button>\n      </Card.Header>\n      <Card.Body>\n        {followingTags?.length ? (\n          <div className=\"m-n1\">\n            {followingTags.map((item) => {\n              const slugName = item?.slug_name;\n              return <Tag key={slugName} className=\"m-1\" data={item} />;\n            })}\n          </div>\n        ) : (\n          <>\n            <div className=\"text-muted\">{t('follow_tag_tip')}</div>\n            <Button\n              size=\"sm\"\n              className=\"mt-3\"\n              variant=\"outline-primary\"\n              onClick={() => setEditState(true)}>\n              {t('follow_a_tag')}\n            </Button>\n          </>\n        )}\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Footer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport Row from 'react-bootstrap/Row';\nimport dayjs from 'dayjs';\n\nimport { siteInfoStore } from '@/stores';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'footer' }); // Scoped translations for footer\n  const fullYear = dayjs().format('YYYY');\n  const siteName = siteInfoStore((state) => state.siteInfo.name);\n  const cc = `${siteName} © ${fullYear}`;\n\n  return (\n    <Row>\n      <footer className=\"py-3 d-flex flex-wrap align-items-center justify-content-between text-secondary small\">\n        <div className=\"d-flex align-items-center\">\n          <div className=\"me-3\">{cc}</div>\n\n          <Link to=\"/tos\" className=\"me-3 link-secondary\">\n            {t('terms', { keyPrefix: 'nav_menus' })}\n          </Link>\n\n          {/* Link to Privacy Policy with right margin for spacing */}\n          <Link to=\"/privacy\" className=\"link-secondary\">\n            {t('privacy', { keyPrefix: 'nav_menus' })}\n          </Link>\n        </div>\n        <div>\n          <Trans i18nKey=\"footer.build_on\" values={{ cc }}>\n            Powered by\n            <a\n              href=\"https://answer.apache.org\"\n              target=\"_blank\"\n              className=\"link-secondary\"\n              rel=\"noreferrer\">\n              Apache Answer\n            </a>\n          </Trans>\n        </div>\n      </footer>\n    </Row>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/components/FormatTime/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\nimport dayjs from 'dayjs';\n\ninterface Props {\n  time: number;\n  className?: string;\n  preFix?: string;\n}\n\nconst Index: FC<Props> = ({ time, preFix, className }) => {\n  const { t } = useTranslation();\n  const formatTime = (from) => {\n    const now = Math.floor(dayjs().valueOf() / 1000);\n    const between = now > from ? now - from : 0;\n\n    if (between <= 1) {\n      return t('dates.now');\n    }\n    if (between > 1 && between < 60) {\n      return t('dates.x_seconds_ago', { count: between });\n    }\n\n    if (between >= 60 && between < 3600) {\n      const min = Math.floor(between / 60);\n      return t('dates.x_minutes_ago', { count: min });\n    }\n    if (between >= 3600 && between < 3600 * 24) {\n      const h = Math.floor(between / 3600);\n      return t('dates.x_hours_ago', { count: h });\n    }\n\n    if (\n      between >= 3600 * 24 &&\n      between < 3600 * 24 * 366 &&\n      dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY')\n    ) {\n      return dayjs.unix(from).tz().format(t('dates.long_date'));\n    }\n\n    return dayjs.unix(from).tz().format(t('dates.long_date_with_year'));\n  };\n\n  if (!time) {\n    return null;\n  }\n\n  return (\n    <time\n      className={classNames('', className)}\n      dateTime={dayjs.unix(time).tz().toISOString()}\n      title={dayjs.unix(time).tz().format(t('dates.long_date_with_time'))}>\n      {preFix ? `${preFix} ` : ''}\n      {formatTime(time)}\n    </time>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Header/components/NavItems/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Nav, Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { NavLink, useNavigate } from 'react-router-dom';\n\nimport type * as Type from '@/common/interface';\nimport { Avatar, Icon } from '@/components';\nimport { floppyNavigation, isDarkTheme } from '@/utils';\nimport { userCenterStore } from '@/stores';\nimport { REACT_BASE_PATH } from '@/router/alias';\n\ninterface Props {\n  redDot: Type.NotificationStatus | undefined;\n  userInfo: Type.UserInfoRes;\n  logOut: (e) => void;\n}\n\nconst Index: FC<Props> = ({ redDot, userInfo, logOut }) => {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const { agent: ucAgent } = userCenterStore();\n  const handleLinkClick = (evt) => {\n    if (floppyNavigation.shouldProcessLinkClick(evt)) {\n      evt.preventDefault();\n      const href = evt.currentTarget.getAttribute('href');\n      floppyNavigation.navigate(href, {\n        handler: navigate,\n      });\n    }\n  };\n  return (\n    <>\n      <Nav className=\"flex-row\">\n        <NavLink\n          to=\"/users/notifications/inbox\"\n          title={t('inbox', { keyPrefix: 'notifications' })}\n          className=\"icon-link nav-link d-flex align-items-center justify-content-center p-0 me-2 position-relative\">\n          <Icon name=\"bell-fill\" className=\"fs-4\" />\n          {(redDot?.inbox || 0) > 0 && (\n            <div className=\"unread-dot bg-danger\">\n              <span className=\"visually-hidden\">\n                {t('new_alerts', { keyPrefix: 'notifications' })}\n              </span>\n            </div>\n          )}\n        </NavLink>\n\n        <NavLink\n          to=\"/users/notifications/achievement\"\n          title={t('achievement', { keyPrefix: 'notifications' })}\n          className=\"icon-link nav-link d-flex align-items-center justify-content-center p-0 me-2 position-relative\">\n          <Icon name=\"trophy-fill\" className=\"fs-4\" />\n          {(redDot?.achievement || 0) > 0 && (\n            <div className=\"unread-dot bg-danger\">\n              <span className=\"visually-hidden\">\n                {t('new_alerts', { keyPrefix: 'notifications' })}\n              </span>\n            </div>\n          )}\n        </NavLink>\n      </Nav>\n\n      <Dropdown align=\"end\" data-bs-theme={isDarkTheme() ? 'dark' : 'light'}>\n        <Dropdown.Toggle\n          variant=\"success\"\n          id=\"dropdown-basic\"\n          as=\"a\"\n          role=\"button\"\n          className=\"no-toggle pointer\">\n          <Avatar\n            size=\"36px\"\n            avatar={userInfo?.avatar}\n            alt={userInfo?.display_name}\n            searchStr=\"s=96\"\n          />\n        </Dropdown.Toggle>\n\n        <Dropdown.Menu className=\"position-absolute\">\n          <Dropdown.Item\n            href={`${REACT_BASE_PATH}/users/${userInfo.username}`}\n            onClick={handleLinkClick}>\n            {t('header.nav.profile')}\n          </Dropdown.Item>\n          <Dropdown.Item\n            href={`${REACT_BASE_PATH}/users/${userInfo.username}/bookmarks`}\n            onClick={handleLinkClick}>\n            {t('header.nav.bookmark')}\n          </Dropdown.Item>\n          <Dropdown.Item\n            href={`${REACT_BASE_PATH}/users/settings/profile`}\n            onClick={handleLinkClick}>\n            {t('header.nav.setting')}\n          </Dropdown.Item>\n          <Dropdown.Divider />\n          <Dropdown.Item\n            href={`${REACT_BASE_PATH}/users/logout`}\n            onClick={(e) => logOut(e)}>\n            {t('header.nav.logout')}\n          </Dropdown.Item>\n        </Dropdown.Menu>\n      </Dropdown>\n      {/* Dropdown for user center agent info */}\n      {ucAgent?.enabled &&\n      (ucAgent?.agent_info?.url ||\n        ucAgent?.agent_info?.control_center?.length) ? (\n        <Dropdown align=\"end\" data-bs-theme={isDarkTheme() ? 'dark' : 'light'}>\n          <Dropdown.Toggle\n            variant=\"success\"\n            id=\"dropdown-uca\"\n            as=\"span\"\n            className=\"no-toggle\">\n            <Nav>\n              <Icon\n                name=\"grid-3x3-gap-fill\"\n                className=\"nav-link pointer p-0 fs-4 ms-3\"\n              />\n            </Nav>\n          </Dropdown.Toggle>\n\n          <Dropdown.Menu className=\"position-absolute\">\n            {ucAgent.agent_info.url ? (\n              <Dropdown.Item href={ucAgent.agent_info.url}>\n                {ucAgent.agent_info.name}\n              </Dropdown.Item>\n            ) : null}\n            {ucAgent.agent_info.url &&\n            ucAgent.agent_info.control_center?.length ? (\n              <Dropdown.Divider />\n            ) : null}\n            {ucAgent.agent_info.control_center?.map((ctrl) => {\n              return (\n                <Dropdown.Item key={ctrl.name} href={ctrl.url}>\n                  {ctrl.label}\n                </Dropdown.Item>\n              );\n            })}\n          </Dropdown.Menu>\n        </Dropdown>\n      ) : null}\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Header/components/SearchInput/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect } from 'react';\nimport { Form, FormControl } from 'react-bootstrap';\nimport { useSearchParams, useNavigate, useLocation } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon } from '@/components';\n\nconst SearchInput: FC<{ className?: string }> = ({ className }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'header' });\n  const navigate = useNavigate();\n  const location = useLocation();\n  const [urlSearch] = useSearchParams();\n  const q = urlSearch.get('q');\n  const [searchStr, setSearch] = useState('');\n  const handleInput = (val) => {\n    setSearch(val);\n  };\n  const handleSearch = (evt) => {\n    evt.preventDefault();\n    if (!searchStr) {\n      return;\n    }\n    const searchUrl = `/search?q=${encodeURIComponent(searchStr)}`;\n    navigate(searchUrl);\n  };\n\n  useEffect(() => {\n    if (q && location.pathname === '/search') {\n      handleInput(q);\n    }\n  }, [q]);\n\n  useEffect(() => {\n    // clear search input when navigate to other page\n    if (location.pathname !== '/search' && searchStr) {\n      setSearch('');\n    }\n  }, [location.pathname]);\n  return (\n    <Form\n      action=\"/search\"\n      className={`w-100 position-relative mx-auto ${className}`}\n      onSubmit={handleSearch}>\n      <div className=\"search-wrap\" onClick={handleSearch}>\n        <Icon name=\"search\" className=\"search-icon\" />\n      </div>\n      <FormControl\n        type=\"search\"\n        placeholder={t('search.placeholder')}\n        className=\"placeholder-search\"\n        value={searchStr}\n        name=\"q\"\n        onChange={(e) => handleInput(e.target.value)}\n      />\n    </Form>\n  );\n};\n\nexport default SearchInput;\n"
  },
  {
    "path": "ui/src/components/Header/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n@import 'bootstrap/scss/functions';\n@import 'bootstrap/scss/variables';\n#header {\n  z-index: 1041;\n  --bs-navbar-padding-y: 0.75rem;\n  background: var(--bs-primary);\n  box-shadow:\n    inset 0 -1px 0 rgba(0, 0, 0, 0.15),\n    0 0.125rem 0.25rem rgb(0 0 0 / 8%);\n  .logo {\n    max-height: 2rem;\n  }\n\n  .fixed-width {\n    padding-left: 28px;\n    padding-right: 36px;\n  }\n\n  .create-icon {\n    color: var(--bs-nav-link-color);\n  }\n\n  .nav-link {\n    &.icon-link {\n      width: 38px;\n      height: 38px;\n    }\n  }\n\n  .search-mobile {\n    color: var(--bs-nav-link-color);\n  }\n\n  .answer-navBar {\n    font-size: 1rem;\n    padding: 0.25rem 0.5rem;\n    border: none;\n  }\n  .answer-navBar:focus {\n    box-shadow: none;\n  }\n\n  .xl-none {\n    display: none !important;\n  }\n\n  .hr {\n    color: var(--bs-navbar-color);\n  }\n\n  // style for colored navbar\n  &.theme-dark {\n    .placeholder-search {\n      padding-left: 42px;\n      box-shadow: none;\n      color: #fff;\n      background-color: rgba(255, 255, 255, 0.2);\n      border: $border-width $border-style rgba(255, 255, 255, 0.2);\n      &:focus {\n        border: $border-width $border-style $border-color;\n      }\n      &::placeholder {\n        color: rgba(255, 255, 255, 0.75);\n      }\n    }\n    .search-icon {\n      color: #fff;\n    }\n  }\n\n  // style for colored navbar\n  &.theme-light {\n    .nav-link {\n      color: rgba(0, 0, 0, 0.65);\n    }\n    .placeholder-search {\n      padding-left: 42px;\n      box-shadow: none;\n      color: var(--bs-body-color);\n      background-color: rgba(255, 255, 255, 0.2);\n      border: $border-width $border-style rgba(0, 0, 0, 0.05);\n      &:focus {\n        border: $border-width $border-style $border-color;\n      }\n      &::placeholder {\n        color: rgba(0, 0, 0, 0.65);\n      }\n    }\n    .search-icon {\n      color: var(--bs-body-color);\n    }\n  }\n\n  .maxw-560 {\n    max-width: 560px;\n  }\n\n  .search-wrap {\n    position: absolute;\n    top: 0;\n    left: 0;\n    padding: 10px 13px;\n    line-height: 1;\n  }\n}\n\n@media (max-width: 1199.9px) {\n  #header {\n    .fixed-width {\n      padding-right: 48px;\n    }\n    .nav-grow {\n      flex-grow: 1 !important;\n    }\n\n    .xl-none {\n      display: flex !important;\n    }\n\n    .maxw-400 {\n      max-width: 100%;\n    }\n  }\n}\n\n@media screen and (max-width: 991px) {\n  #header {\n    .fixed-width {\n      padding-left: 40px;\n      padding-right: 48px;\n    }\n  }\n}\n\n@media screen and (max-width: 767px) {\n  #header {\n    .fixed-width {\n      padding-left: 1.5rem;\n      padding-right: 1.5rem;\n    }\n    .nav-text {\n      flex: 1;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      min-width: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/components/Header/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useState, useEffect } from 'react';\nimport { Navbar, Nav, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link, NavLink, useLocation, useMatch } from 'react-router-dom';\n\nimport classnames from 'classnames';\n\nimport { userCenter, floppyNavigation, isLight } from '@/utils';\nimport {\n  loggedUserInfoStore,\n  siteInfoStore,\n  brandingStore,\n  loginSettingStore,\n  themeSettingStore,\n  sideNavStore,\n} from '@/stores';\nimport { logout, useQueryNotificationStatus } from '@/services';\nimport { Icon, MobileSideNav } from '@/components';\n\nimport NavItems from './components/NavItems';\nimport SearchInput from './components/SearchInput';\n\nimport './index.scss';\n\nconst Header: FC = () => {\n  const location = useLocation();\n  const { user, clear: clearUserStore } = loggedUserInfoStore();\n  const { t } = useTranslation();\n  const siteInfo = siteInfoStore((state) => state.siteInfo);\n  const brandingInfo = brandingStore((state) => state.branding);\n  const loginSetting = loginSettingStore((state) => state.login);\n  const { updateReview } = sideNavStore();\n  const { data: redDot } = useQueryNotificationStatus();\n  const [showMobileSideNav, setShowMobileSideNav] = useState(false);\n\n  const [showMobileSearchInput, setShowMobileSearchInput] = useState(false);\n  /**\n   * Automatically append `tag` information when creating a question\n   */\n  const tagMatch = useMatch('/tags/:slugName');\n  let askUrl = '/questions/add';\n  if (tagMatch && tagMatch.params.slugName) {\n    askUrl = `${askUrl}?tags=${encodeURIComponent(tagMatch.params.slugName)}`;\n  }\n\n  useEffect(() => {\n    updateReview({\n      can_revision: Boolean(redDot?.can_revision),\n      revision: Number(redDot?.revision),\n    });\n  }, [redDot]);\n\n  const handleLogout = async (evt) => {\n    evt.preventDefault();\n    await logout();\n    clearUserStore();\n    window.location.replace(window.location.href);\n  };\n\n  useEffect(() => {\n    setShowMobileSearchInput(false);\n    setShowMobileSideNav(false);\n  }, [location.pathname]);\n\n  let navbarStyle = 'theme-light';\n  let themeMode = 'light';\n  const { theme, theme_config, layout } = themeSettingStore((_) => _);\n  if (theme_config?.[theme]?.navbar_style) {\n    // const color = theme_config[theme].navbar_style.startsWith('#')\n    themeMode = isLight(theme_config[theme].navbar_style) ? 'light' : 'dark';\n    navbarStyle = `theme-${themeMode}`;\n  }\n\n  useEffect(() => {\n    const handleResize = () => {\n      if (window.innerWidth >= 1199.9) {\n        setShowMobileSideNav(false);\n        setShowMobileSearchInput(false);\n      }\n    };\n\n    window.addEventListener('resize', handleResize);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n    };\n  }, []);\n\n  return (\n    <Navbar\n      data-bs-theme={themeMode}\n      expand=\"xl\"\n      className={classnames('sticky-top', navbarStyle)}\n      style={{\n        backgroundColor: theme_config[theme].navbar_style,\n      }}\n      id=\"header\">\n      <div\n        className={classnames(\n          'w-100 d-flex align-items-center',\n          layout === 'Fixed-width' ? 'container-xxl fixed-width' : 'px-3',\n        )}>\n        <Navbar.Toggle\n          className=\"answer-navBar me-2\"\n          onClick={() => {\n            setShowMobileSideNav(!showMobileSideNav);\n            setShowMobileSearchInput(false);\n          }}\n        />\n\n        <Navbar.Brand\n          to=\"/\"\n          as={Link}\n          className=\"lh-1 me-0 me-sm-5 p-0 nav-text\">\n          {brandingInfo.logo ? (\n            <>\n              <img\n                className=\"d-none d-xl-block logo me-0\"\n                src={brandingInfo.logo}\n                alt={siteInfo.name}\n              />\n\n              <img\n                className=\"xl-none logo me-0\"\n                src={brandingInfo.mobile_logo || brandingInfo.logo}\n                alt={siteInfo.name}\n              />\n            </>\n          ) : (\n            <span>{siteInfo.name}</span>\n          )}\n        </Navbar.Brand>\n\n        <SearchInput className=\"d-none d-lg-block maxw-560\" />\n\n        <Nav className=\"d-block d-lg-none me-2 ms-auto\">\n          <Button\n            variant=\"link\"\n            onClick={() => {\n              setShowMobileSideNav(false);\n              setShowMobileSearchInput(!showMobileSearchInput);\n            }}\n            className=\"p-0 btn-no-border icon-link nav-link d-flex align-items-center justify-content-center\">\n            <Icon name=\"search\" className=\"lh-1 fs-4\" />\n          </Button>\n        </Nav>\n\n        {/* pc nav */}\n        {user?.username ? (\n          <Nav className=\"d-flex align-items-center flex-nowrap flex-row\">\n            <Nav.Item className=\"me-2 d-block d-xl-none\">\n              <NavLink\n                to={askUrl}\n                className=\"d-block icon-link nav-link text-center\">\n                <Icon name=\"plus-lg\" className=\"lh-1 fs-4\" />\n              </NavLink>\n            </Nav.Item>\n\n            <Nav.Item className=\"me-2 d-none d-xl-block\">\n              <NavLink\n                to={askUrl}\n                className=\"nav-link d-flex align-items-center text-capitalize text-nowrap\">\n                <Icon name=\"plus-lg\" className=\"me-2 lh-1 fs-4\" />\n                <span>{t('btns.create')}</span>\n              </NavLink>\n            </Nav.Item>\n\n            <NavItems redDot={redDot} userInfo={user} logOut={handleLogout} />\n          </Nav>\n        ) : (\n          <>\n            <Link\n              className={classnames('me-2 btn btn-link', {\n                'link-light': navbarStyle === 'theme-dark',\n                'link-primary': navbarStyle !== 'theme-dark',\n              })}\n              onClick={() => floppyNavigation.storageLoginRedirect()}\n              to={userCenter.getLoginUrl()}>\n              {t('btns.login')}\n            </Link>\n            {loginSetting.allow_new_registrations && (\n              <Link\n                className={classnames(\n                  'btn',\n                  navbarStyle === 'theme-dark' ? 'btn-light' : 'btn-primary',\n                )}\n                to={userCenter.getSignUpUrl()}>\n                {t('btns.signup')}\n              </Link>\n            )}\n          </>\n        )}\n      </div>\n\n      {showMobileSearchInput && (\n        <div className=\"w-100 px-3 mt-2 d-block d-lg-none\">\n          <SearchInput />\n        </div>\n      )}\n\n      <MobileSideNav show={showMobileSideNav} onHide={setShowMobileSideNav} />\n    </Navbar>\n  );\n};\n\nexport default memo(Header);\n"
  },
  {
    "path": "ui/src/components/HighlightText/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.highlight-text > mark {\n  padding: 0;\n}\n"
  },
  {
    "path": "ui/src/components/HighlightText/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\n\nimport './index.scss';\n\ninterface IProps {\n  text: string;\n  keywords: string[];\n}\n\nconst Index: FC<IProps> = ({ text = '', keywords = [] }) => {\n  const regex = new RegExp(`(${keywords.join('|')})`, 'gi');\n\n  return (\n    <span className=\"highlight-text\">\n      {text.split(regex).map((piece: string, index: number) => {\n        const key = `${piece.substring(0, 6)}_${index}`;\n        return keywords.find(\n          (kw: string) => kw.toLocaleLowerCase() === piece.toLocaleLowerCase(),\n        ) ? (\n          <mark key={key}>{piece}</mark>\n        ) : (\n          piece\n        );\n      })}\n    </span>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/HotQuestions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport { Icon } from '@/components';\nimport { useHotQuestions } from '@/services';\n\nconst HotQuestions: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  const { data: questionRes } = useHotQuestions();\n  if (!questionRes?.list?.length) {\n    return null;\n  }\n  return (\n    <Card>\n      <Card.Header className=\"text-nowrap text-capitalize\">\n        {t('hot_questions')}\n      </Card.Header>\n      <ListGroup variant=\"flush\">\n        {questionRes?.list?.map((li) => {\n          return (\n            <ListGroupItem\n              key={li.id}\n              as={Link}\n              to={pathFactory.questionLanding(li.id, li.url_title)}\n              action>\n              <div className=\"link-dark text-truncate-3\">{li.title}</div>\n              {li.answer_count > 0 ? (\n                <div\n                  className={`d-flex align-items-center small mt-1 ${\n                    li.accepted_answer_id > 0\n                      ? 'link-success'\n                      : 'link-secondary'\n                  }`}>\n                  {li.accepted_answer_id >= 1 ? (\n                    <Icon name=\"check-circle-fill\" />\n                  ) : (\n                    <Icon name=\"chat-square-text-fill\" />\n                  )}\n                  <span className=\"ms-1\">\n                    {t('x_answers', { count: li.answer_count })}\n                  </span>\n                </div>\n              ) : null}\n            </ListGroupItem>\n          );\n        })}\n      </ListGroup>\n    </Card>\n  );\n};\nexport default HotQuestions;\n"
  },
  {
    "path": "ui/src/components/HttpErrorContent/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, useEffect } from 'react';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\n\nconst Index = ({\n  httpCode = '',\n  title = '',\n  errMsg = '',\n  showErrorCode = true,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_error' });\n  useEffect(() => {\n    // auto height of container\n    const pageWrap = document.querySelector('.page-wrap') as HTMLElement;\n    if (pageWrap) {\n      pageWrap.style.display = 'contents';\n    }\n\n    return () => {\n      if (pageWrap) {\n        pageWrap.style.display = 'block';\n      }\n    };\n  }, []);\n\n  usePageTags({\n    title: t(`http_${httpCode}`, { keyPrefix: 'page_title' }),\n  });\n\n  return (\n    <div className=\"d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center\">\n      <div\n        className=\"mb-4 text-secondary\"\n        style={{ fontSize: '120px', lineHeight: 1.2 }}>\n        (=‘x‘=)\n      </div>\n      {showErrorCode && (\n        <h4 className=\"text-center\">{t('http_error', { code: httpCode })}</h4>\n      )}\n      {title && <h4 className=\"text-center\">{title}</h4>}\n      <div className=\"text-center mb-3 fs-5\">\n        {errMsg || t(`desc_${httpCode}`)}\n      </div>\n      <div className=\"text-center\">\n        <Link to=\"/\" className=\"btn btn-link\">\n          {t('back_home')}\n        </Link>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Icon/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\n\nimport classNames from 'classnames';\n\ninterface IProps {\n  type?: 'br' | 'bi';\n  /** icon name */\n  name: string;\n  className?: string;\n  size?: string;\n  title?: string;\n  onClick?: () => void;\n}\nconst Icon: FC<IProps> = ({\n  type = 'br',\n  name,\n  className,\n  size,\n  onClick,\n  title,\n}) => {\n  return (\n    <i\n      className={classNames(type, `bi-${name}`, className)}\n      style={{ ...(size && { fontSize: size }) }}\n      onClick={onClick}\n      onKeyDown={onClick}\n      title={title}\n    />\n  );\n};\n\nexport default Icon;\n"
  },
  {
    "path": "ui/src/components/Icon/svg.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\n\nimport { base64ToSvg } from '@/utils';\n\ninterface IProps {\n  svgClassName?: string;\n  base64: string | undefined;\n}\nconst Icon: FC<IProps> = ({ base64 = '', svgClassName = '' }) => {\n  return base64 ? (\n    <span\n      dangerouslySetInnerHTML={{\n        __html: base64ToSvg(base64, svgClassName),\n      }}\n    />\n  ) : null;\n};\n\nexport default Icon;\n"
  },
  {
    "path": "ui/src/components/ImgViewer/index.css",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.img-viewer .cursor-zoom-out {\n  cursor: zoom-out !important;\n}\n\n.img-viewer img:not(a img, img.broken, img.invisible) {\n  cursor: zoom-in;\n}\n"
  },
  {
    "path": "ui/src/components/ImgViewer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';\nimport { Modal } from 'react-bootstrap';\n\nimport './index.css';\nimport classnames from 'classnames';\n\nconst Index: FC<{\n  children: ReactNode;\n  className?: classnames.Argument;\n}> = ({ children, className }) => {\n  const [visible, setVisible] = useState(false);\n  const [imgSrc, setImgSrc] = useState('');\n  const onClose = () => {\n    setVisible(false);\n    setImgSrc('');\n  };\n\n  const checkIfInLink = (target) => {\n    let ret = false;\n    let el = target.parentElement;\n    while (el) {\n      if (el.nodeName.toLowerCase() === 'a') {\n        ret = true;\n        break;\n      }\n      el = el.parentElement;\n    }\n    return ret;\n  };\n\n  const checkClickForImgView = (evt: MouseEvent<HTMLElement>) => {\n    const { target } = evt;\n    // @ts-ignore\n    if (target.nodeName.toLowerCase() !== 'img') {\n      return;\n    }\n    const img = target as HTMLImageElement;\n    if (!img.naturalWidth || !img.naturalHeight) {\n      img.classList.add('broken');\n      return;\n    }\n    const src = img.currentSrc || img.src;\n    if (src && checkIfInLink(img) === false) {\n      setImgSrc(src);\n      setVisible(true);\n    }\n  };\n\n  useEffect(() => {\n    return () => {\n      onClose();\n    };\n  }, []);\n\n  return (\n    // eslint-disable-next-line jsx-a11y/click-events-have-key-events\n    <div\n      className={classnames('img-viewer', className)}\n      onClick={checkClickForImgView}>\n      {children}\n      <Modal\n        show={visible}\n        fullscreen\n        centered\n        scrollable\n        contentClassName=\"bg-transparent\"\n        onHide={onClose}>\n        <Modal.Body onClick={onClose} className=\"img-viewer p-0 d-flex\">\n          {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}\n          <img\n            className=\"cursor-zoom-out img-fluid m-auto\"\n            onClick={(evt) => {\n              evt.stopPropagation();\n              onClose();\n            }}\n            src={imgSrc}\n            alt={imgSrc}\n          />\n        </Modal.Body>\n      </Modal>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/InitialLoadingPlaceholder/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Same as spin in `public/index.html`\n\n@keyframes _initial-loading-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.InitialLoadingPlaceholder {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background-color: white;\n  z-index: 9999;\n\n  &-spinnerContainer {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n  }\n\n  &-spinner {\n    box-sizing: border-box;\n    display: inline-block;\n    width: 2rem;\n    height: 2rem;\n    vertical-align: -0.125em;\n    border: 0.25rem solid currentColor;\n    border-right-color: transparent;\n    color: rgba(108, 117, 125, 0.75);\n    border-radius: 50%;\n    animation: 0.75s linear infinite _initial-loading-spin;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/InitialLoadingPlaceholder/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// Same as spin in `public/index.html`\n\nimport './index.scss';\n\nfunction InitialLoadingPlaceholder() {\n  return (\n    <div className=\"InitialLoadingPlaceholder\">\n      <div className=\"InitialLoadingPlaceholder-spinnerContainer\">\n        <div className=\"InitialLoadingPlaceholder-spinner\" />\n      </div>\n    </div>\n  );\n}\n\nexport default InitialLoadingPlaceholder;\n"
  },
  {
    "path": "ui/src/components/Mentions/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.bg-gray-200 {\n  color: var(--bs-body-color);\n  background-color: var(--an-invite-answer-item-active);\n}\n"
  },
  {
    "path": "ui/src/components/Mentions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useEffect, useRef, useState, FC } from 'react';\nimport { Dropdown } from 'react-bootstrap';\n\nimport { useSearchUserStaff } from '@/services';\nimport * as Types from '@/common/interface';\n\nimport './index.scss';\n\ninterface IProps {\n  children: React.ReactNode;\n  pageUsers;\n  onSelected: (val: string) => void;\n}\n\nconst MAX_RECODE = 5;\n\nconst Mentions: FC<IProps> = ({ children, pageUsers, onSelected }) => {\n  const menuRef = useRef<HTMLDivElement>(null);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const [val, setValue] = useState('');\n  const [users, setUsers] = useState<Types.PageUser[]>([]);\n  const [cursor, setCursor] = useState(0);\n  const [isRequested, setRequestedState] = useState(false);\n  const { data: staffUserList = [] } = useSearchUserStaff(val);\n  const mapStaffUsers =\n    staffUserList\n      ?.map((item) => ({\n        displayName: item.display_name,\n        userName: item.username,\n      }))\n      ?.filter(\n        (item) =>\n          users.findIndex((user) => user.userName === item.userName) < 0,\n      ) || [];\n\n  const searchUser = () => {\n    const element = dropdownRef.current?.children[0];\n    const { value, selectionStart = 0 } = element as HTMLTextAreaElement;\n\n    if (value.indexOf('@') < 0) {\n      setValue('');\n    }\n    if (!selectionStart) {\n      return;\n    }\n\n    const str = value.substring(\n      value.substring(0, selectionStart).lastIndexOf('@'),\n      selectionStart,\n    );\n\n    if (str.substring(str.lastIndexOf(' '), selectionStart).indexOf('@') < 0) {\n      return;\n    }\n\n    setValue(str.substring(1));\n\n    if (!str.substring(1)) {\n      return;\n    }\n    if (isRequested) {\n      return;\n    }\n    setRequestedState(true);\n  };\n\n  useEffect(() => {\n    const element = dropdownRef.current?.children[0] as HTMLTextAreaElement;\n\n    if (element) {\n      element.addEventListener('input', searchUser);\n    }\n    return () => {\n      element.removeEventListener('input', searchUser);\n    };\n  }, [dropdownRef]);\n\n  useEffect(() => {\n    setUsers(pageUsers);\n  }, [pageUsers, val]);\n\n  const handleClick = (item) => {\n    const element = dropdownRef.current?.children[0] as HTMLTextAreaElement;\n\n    const { value, selectionStart = 0 } = element;\n\n    if (!selectionStart) {\n      return;\n    }\n\n    const text = `@${item?.userName} `;\n    onSelected(\n      `${value.substring(\n        0,\n        value.substring(0, selectionStart).lastIndexOf('@'),\n      )}${text}${value.substring(selectionStart)}`,\n    );\n    setUsers([]);\n    setValue('');\n  };\n  const filterData = val\n    ? [...users, ...mapStaffUsers].filter(\n        (item) =>\n          item.displayName?.indexOf(val) === 0 ||\n          item.userName?.indexOf(val) === 0,\n      )\n    : [];\n  const handleKeyDown = (e) => {\n    const { keyCode } = e;\n\n    if (keyCode === 38 && cursor > 0) {\n      e.preventDefault();\n      setCursor(cursor - 1);\n    }\n    if (keyCode === 40 && cursor < filterData.length - 1) {\n      e.preventDefault();\n\n      setCursor(cursor + 1);\n    }\n    if (keyCode === 13 && cursor > -1 && cursor <= filterData.length - 1) {\n      e.preventDefault();\n\n      const item = filterData[cursor];\n\n      handleClick(item);\n      setCursor(0);\n    }\n  };\n\n  return (\n    <Dropdown\n      ref={dropdownRef}\n      className=\"mentions-wrap\"\n      show={filterData.length > 0}\n      onKeyDown={handleKeyDown}>\n      {children}\n      <Dropdown.Menu\n        className={filterData.length > 0 ? 'visible' : 'invisible'}\n        ref={menuRef}>\n        {filterData\n          .filter((_, index) => index < MAX_RECODE)\n          .map((item, index) => {\n            return (\n              <Dropdown.Item\n                className={`${cursor === index ? 'bg-gray-200' : ''}`}\n                key={item.displayName}\n                onClick={() => handleClick(item)}>\n                <span className=\"link-dark me-1\">{item.displayName}</span>\n                <small className=\"link-secondary\">@{item.userName}</small>\n              </Dropdown.Item>\n            );\n          })}\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default Mentions;\n"
  },
  {
    "path": "ui/src/components/MobileSideNav/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n#mobileSideNav {\n  top: 62px !important;\n  width: 240px !important;\n  height: calc(100vh - 62px) !important;\n  overflow-y: auto;\n  flex: none;\n  .navbar-nav {\n    --bs-nav-link-padding-x: 1rem;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/MobileSideNav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Offcanvas } from 'react-bootstrap';\nimport { useLocation } from 'react-router-dom';\n\nimport { SideNav, AdminSideNav } from '@/components';\n\nimport './index.scss';\n\nconst MobileSideNav = ({ show, onHide }) => {\n  const { pathname } = useLocation();\n  const isAdmin = pathname.includes('/admin');\n  return (\n    <Offcanvas\n      show={show}\n      onHide={() => {\n        onHide(false);\n      }}\n      id=\"mobileSideNav\"\n      className=\"px-3 py-4\">\n      <Offcanvas.Body className=\"p-0\">\n        {isAdmin ? <AdminSideNav /> : <SideNav />}\n      </Offcanvas.Body>\n    </Offcanvas>\n  );\n};\n\nexport default MobileSideNav;\n"
  },
  {
    "path": "ui/src/components/Modal/BadgeModal.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useNavigate } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport type * as Type from '@/common/interface';\nimport { loggedUserInfoStore } from '@/stores';\nimport { readNotification, useQueryNotificationStatus } from '@/services';\nimport AnimateGift from '@/utils/animateGift';\nimport Icon from '../Icon';\n\nimport Modal from './Modal';\n\ninterface BadgeModalProps {\n  badge?: Type.NotificationBadgeAward | null;\n  visible: boolean;\n}\n\nlet bg1: AnimateGift;\nlet bg2: AnimateGift;\nlet timeout: NodeJS.Timeout;\nconst BadgeModal: FC<BadgeModalProps> = ({ badge, visible }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges.modal' });\n  const { user } = loggedUserInfoStore();\n  const navigate = useNavigate();\n  const { data, mutate } = useQueryNotificationStatus();\n\n  const handle = async () => {\n    if (!data) return;\n    await readNotification(badge?.notification_id);\n    await mutate({\n      ...data,\n      badge_award: null,\n    });\n    clearTimeout(timeout);\n    bg1?.destroy();\n    bg2?.destroy();\n  };\n  const handleCancel = async () => {\n    await handle();\n  };\n  const handleConfirm = async () => {\n    await handle();\n\n    const url = `/badges/${badge?.badge_id}?username=${user.username}`;\n    navigate(url);\n  };\n\n  const initAnimation = () => {\n    const DURATION = 8000;\n    const LENGTH = 200;\n    const bgNode = document.documentElement || document.body;\n    const badgeModalNode = document.getElementById('badgeModal');\n    const parentNode = badgeModalNode?.parentNode;\n\n    badgeModalNode?.setAttribute('style', 'z-index: 1');\n\n    if (parentNode) {\n      bg1 = new AnimateGift({\n        elm: parentNode,\n        width: bgNode.clientWidth,\n        height: bgNode.clientHeight,\n        length: LENGTH,\n        duration: DURATION,\n        isLoop: true,\n      });\n\n      timeout = setTimeout(() => {\n        bg2 = new AnimateGift({\n          elm: parentNode,\n          width: window.innerWidth,\n          height: window.innerHeight,\n          length: LENGTH,\n          duration: DURATION,\n        });\n      }, DURATION / 2);\n    }\n  };\n\n  const destroyAnimation = () => {\n    clearTimeout(timeout);\n    bg1?.destroy();\n    bg2?.destroy();\n  };\n\n  useEffect(() => {\n    if (visible) {\n      initAnimation();\n    } else {\n      destroyAnimation();\n    }\n\n    const handleVisibilityChange = () => {\n      if (document.visibilityState === 'visible') {\n        initAnimation();\n      } else {\n        destroyAnimation();\n      }\n    };\n    document.addEventListener('visibilitychange', handleVisibilityChange);\n\n    return () => {\n      document.removeEventListener('visibilitychange', handleVisibilityChange);\n      destroyAnimation();\n    };\n  }, [visible]);\n\n  return (\n    <Modal\n      id=\"badgeModal\"\n      title={t('title')}\n      visible={visible}\n      onCancel={handleCancel}\n      onConfirm={handleConfirm}\n      cancelText={t('close')}\n      cancelBtnVariant=\"link\"\n      confirmText={t('confirm')}\n      confirmBtnVariant=\"primary\"\n      scrollable={false}>\n      {badge && (\n        <div className=\"text-center\">\n          {badge.icon?.startsWith('http') ? (\n            <img src={badge.icon} width={96} height={96} alt={badge.name} />\n          ) : (\n            <Icon\n              name={badge.icon}\n              size=\"96px\"\n              className={classNames(\n                'lh-1',\n                badge.level === 1 && 'bronze',\n                badge.level === 2 && 'silver',\n                badge.level === 3 && 'gold',\n              )}\n            />\n          )}\n          <h5 className=\"mt-3\">{badge?.name}</h5>\n          <p>{t('content')}</p>\n        </div>\n      )}\n    </Modal>\n  );\n};\n\nexport default BadgeModal;\n"
  },
  {
    "path": "ui/src/components/Modal/Confirm.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n/* eslint-disable @typescript-eslint/no-use-before-define */\nimport * as React from 'react';\n\nimport ReactDOM from 'react-dom/client';\n\nimport Modal from './Modal';\nimport type { Props } from './Modal';\n\nconst div = document.createElement('div');\n\nconst root = ReactDOM.createRoot(div);\n\nexport interface Config extends Props {\n  content: string;\n}\n\nconst Index = ({\n  title = '',\n  confirmText = '',\n  content,\n  onCancel: onClose,\n  onConfirm,\n  cancelBtnVariant = 'link',\n  confirmBtnVariant = 'primary',\n  ...props\n}: Config) => {\n  const onCancel = () => {\n    if (typeof onClose === 'function') {\n      onClose();\n    }\n    render({ visible: false });\n    div.remove();\n  };\n  const onOk = (e) => {\n    if (typeof onConfirm === 'function') {\n      onConfirm(e);\n    }\n    onCancel();\n  };\n  function render({ visible }: { visible: boolean }) {\n    root.render(\n      <Modal\n        visible={visible}\n        title={title}\n        centered={false}\n        onCancel={onCancel}\n        onConfirm={onOk}\n        confirmText={confirmText}\n        cancelBtnVariant={cancelBtnVariant}\n        confirmBtnVariant={confirmBtnVariant}\n        {...props}>\n        <p dangerouslySetInnerHTML={{ __html: content }} />\n      </Modal>,\n    );\n  }\n  render({ visible: true });\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/Modal/LoginToContinueModal.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React from 'react';\nimport { Modal } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { loginToContinueStore, siteInfoStore } from '@/stores';\nimport { floppyNavigation } from '@/utils';\nimport { WelcomeTitle } from '@/components';\n\nimport './login.scss';\n\ninterface IProps {\n  visible: boolean;\n}\n\nconst Index: React.FC<IProps> = ({ visible = false }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'login' });\n  const { update: updateStore } = loginToContinueStore();\n  const { siteInfo } = siteInfoStore((_) => _);\n  const closeModal = () => {\n    updateStore({ show: false });\n  };\n  const linkClick = (evt) => {\n    evt.stopPropagation();\n    floppyNavigation.storageLoginRedirect();\n    closeModal();\n  };\n  return (\n    <Modal\n      show={visible}\n      onHide={closeModal}\n      centered\n      className=\"loginToContinueModal\"\n      fullscreen=\"sm-down\">\n      <Modal.Header closeButton>\n        <Modal.Title as=\"h5\">{t('login_to_continue')}</Modal.Title>\n      </Modal.Header>\n      <Modal.Body className=\"p-5\">\n        <div className=\"d-flex flex-column align-items-center text-center text-body\">\n          <WelcomeTitle className=\"mb-2\" />\n          <p>{siteInfo.description}</p>\n        </div>\n        <div className=\"d-grid gap-2\">\n          <Link\n            to=\"/users/login\"\n            className=\"btn btn-primary\"\n            onClick={linkClick}>\n            {t('login', { keyPrefix: 'btns' })}\n          </Link>\n          <Link\n            to=\"/users/register\"\n            className=\"btn btn-link\"\n            onClick={linkClick}>\n            {t('signup', { keyPrefix: 'btns' })}\n          </Link>\n        </div>\n      </Modal.Body>\n    </Modal>\n  );\n};\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/Modal/Modal.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Button, Modal } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nexport interface Props {\n  id?: string;\n  /** header title */\n  title?: string;\n  children?: React.ReactNode;\n  /** visible */\n  visible?: boolean;\n  centered?: boolean;\n  onCancel?: () => void;\n  onConfirm?: (event: any) => void;\n  cancelText?: string;\n  showCancel?: boolean;\n  cancelBtnVariant?: string;\n  confirmText?: string;\n  showConfirm?: boolean;\n  confirmBtnDisabled?: boolean;\n  confirmBtnVariant?: string;\n  /** body style */\n  bodyClass?: string;\n  scrollable?: boolean;\n  className?: string;\n}\nconst Index: FC<Props> = ({\n  id = '',\n  title = 'title',\n  visible = false,\n  centered = true,\n  onCancel,\n  children,\n  onConfirm,\n  cancelText = '',\n  showCancel = true,\n  cancelBtnVariant = 'primary',\n  confirmText = '',\n  showConfirm = true,\n  confirmBtnVariant = 'link',\n  confirmBtnDisabled = false,\n  bodyClass = '',\n  scrollable = false,\n  className = '',\n}) => {\n  const { t } = useTranslation();\n  return (\n    <Modal\n      id={id}\n      className={className}\n      scrollable={scrollable}\n      show={visible}\n      onHide={onCancel}\n      centered={centered}\n      fullscreen=\"sm-down\">\n      <Modal.Header closeButton>\n        <Modal.Title as=\"h5\">\n          {title || t('title', { keyPrefix: 'modal_confirm' })}\n        </Modal.Title>\n      </Modal.Header>\n      <Modal.Body className={classNames('text-break', bodyClass)}>\n        {children}\n      </Modal.Body>\n      {(showCancel || showConfirm) && (\n        <Modal.Footer>\n          {showCancel && (\n            <Button variant={cancelBtnVariant} onClick={onCancel}>\n              {cancelText === 'close'\n                ? t('btns.close')\n                : cancelText || t('btns.cancel')}\n            </Button>\n          )}\n          {showConfirm && (\n            <Button\n              variant={confirmBtnVariant}\n              onClick={(event) => {\n                onConfirm?.(event);\n              }}\n              id=\"ok_button\"\n              disabled={confirmBtnDisabled}>\n              {confirmText === 'OK'\n                ? t('btns.ok')\n                : confirmText || t('btns.confirm')}\n            </Button>\n          )}\n        </Modal.Footer>\n      )}\n    </Modal>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/components/Modal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport DefaultModal from './Modal';\nimport confirm, { Config } from './Confirm';\nimport LoginToContinueModal from './LoginToContinueModal';\nimport BadgeModal from './BadgeModal';\n\ntype ModalType = typeof DefaultModal & {\n  confirm: (config: Config) => void;\n};\nconst Modal = DefaultModal as ModalType;\n\nModal.confirm = function (props: Config) {\n  return confirm(props);\n};\n\nexport default Modal;\n\nexport { LoginToContinueModal, BadgeModal };\n"
  },
  {
    "path": "ui/src/components/Modal/login.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.modal-backdrop {\n  z-index: 1081;\n}\n\n.loginToContinueModal.show {\n  z-index: 1082;\n}\n"
  },
  {
    "path": "ui/src/components/Operate/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Button, Dropdown } from 'react-bootstrap';\nimport { Link, useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon, Modal } from '@/components';\nimport { useReportModal, useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { QuestionOperationReq } from '@/common/interface';\nimport Share from '../Share';\nimport {\n  deleteQuestion,\n  deleteAnswer,\n  editCheck,\n  reopenQuestion,\n  questionOperation,\n  unDeleteAnswer,\n  unDeleteQuestion,\n} from '@/services';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { floppyNavigation } from '@/utils';\nimport { toastStore } from '@/stores';\n\nimport '@/components/QueryGroup/index.scss';\n\ninterface IProps {\n  type: 'answer' | 'question';\n  qid: string;\n  aid?: string;\n  title: string;\n  hasAnswer?: boolean;\n  isAccepted: boolean;\n  callback: (type: string) => void;\n  memberActions;\n}\nconst Index: FC<IProps> = ({\n  type,\n  qid,\n  aid = '',\n  title,\n  isAccepted = false,\n  hasAnswer = false,\n  memberActions = [],\n  callback,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'delete' });\n  const toast = useToast();\n  const navigate = useNavigate();\n  const reportModal = useReportModal();\n  const dCaptcha = useCaptchaPlugin('delete');\n\n  const refreshQuestion = () => {\n    callback?.('default');\n  };\n  const closeModal = useReportModal(refreshQuestion);\n  const editUrl =\n    type === 'answer' ? `/posts/${qid}/${aid}/edit` : `/posts/${qid}/edit`;\n\n  const handleReport = () => {\n    reportModal.onShow({\n      type,\n      id: type === 'answer' ? aid : qid,\n      action: 'flag',\n    });\n  };\n\n  const handleClose = () => {\n    closeModal.onShow({\n      type,\n      id: qid,\n      action: 'close',\n    });\n  };\n\n  const submitDeleteQuestion = () => {\n    const req = {\n      id: qid,\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    dCaptcha?.resolveCaptchaReq(req);\n\n    deleteQuestion(req)\n      .then(async () => {\n        await dCaptcha?.close();\n        toast.onShow({\n          msg: t('post_deleted', { keyPrefix: 'messages' }),\n          variant: 'success',\n        });\n        callback?.('delete_question');\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          dCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n\n  const submitDeleteAnswer = () => {\n    const req = {\n      id: aid,\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    dCaptcha?.resolveCaptchaReq(req);\n\n    deleteAnswer(req)\n      .then(async () => {\n        await dCaptcha?.close();\n        // refresh page\n        toast.onShow({\n          msg: t('tip_answer_deleted'),\n          variant: 'success',\n        });\n        callback?.('delete_answer');\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          dCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n\n  const handleDelete = () => {\n    if (type === 'question') {\n      Modal.confirm({\n        title: t('title'),\n        content: hasAnswer ? t('question') : t('other'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          if (!dCaptcha) {\n            submitDeleteQuestion();\n            return;\n          }\n          dCaptcha.check(() => {\n            submitDeleteQuestion();\n          });\n        },\n      });\n    }\n\n    if (type === 'answer' && aid) {\n      Modal.confirm({\n        title: t('title'),\n        content: isAccepted ? t('answer_accepted') : t('other'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          if (!dCaptcha) {\n            submitDeleteAnswer();\n            return;\n          }\n          dCaptcha.check(() => {\n            submitDeleteAnswer();\n          });\n        },\n      });\n    }\n  };\n\n  const handleUndelete = () => {\n    Modal.confirm({\n      title: t('undelete_title'),\n      content: t('undelete_desc'),\n      cancelBtnVariant: 'link',\n      confirmBtnVariant: 'danger',\n      confirmText: t('undelete', { keyPrefix: 'btns' }),\n      onConfirm: () => {\n        if (type === 'question') {\n          unDeleteQuestion(qid).then(() => {\n            callback?.('default');\n          });\n        }\n\n        if (type === 'answer') {\n          unDeleteAnswer(aid).then(() => {\n            callback?.('all');\n          });\n        }\n      },\n    });\n  };\n\n  const handleEdit = (evt, targetUrl) => {\n    if (!floppyNavigation.shouldProcessLinkClick(evt)) {\n      return;\n    }\n    evt.preventDefault();\n    let checkObjectId = qid;\n    if (type === 'answer') {\n      checkObjectId = aid;\n    }\n    editCheck(checkObjectId).then(() => {\n      navigate(targetUrl);\n    });\n  };\n\n  const handleReopen = () => {\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'question_detail.reopen' }),\n      content: t('content', { keyPrefix: 'question_detail.reopen' }),\n      cancelBtnVariant: 'link',\n      confirmText: t('confirm_btn', { keyPrefix: 'question_detail.reopen' }),\n      onConfirm: () => {\n        reopenQuestion({\n          question_id: qid,\n        }).then(() => {\n          toast.onShow({\n            msg: t('post_reopen', { keyPrefix: 'messages' }),\n            variant: 'success',\n          });\n          refreshQuestion();\n        });\n      },\n    });\n  };\n\n  const handleCommon = async (params) => {\n    await questionOperation(params);\n    let msg = '';\n    if (params.operation === 'pin') {\n      msg = t('post_pin', { keyPrefix: 'messages' });\n    }\n    if (params.operation === 'unpin') {\n      msg = t('post_unpin', { keyPrefix: 'messages' });\n    }\n    if (params.operation === 'hide') {\n      msg = t('post_hide_list', { keyPrefix: 'messages' });\n    }\n    if (params.operation === 'show') {\n      msg = t('post_show_list', { keyPrefix: 'messages' });\n    }\n    toastStore.getState().show({\n      msg,\n      variant: 'success',\n    });\n    setTimeout(() => {\n      refreshQuestion();\n    }, 100);\n  };\n\n  const handlOtherActions = (action) => {\n    const params: QuestionOperationReq = {\n      id: qid,\n      operation: action,\n    };\n\n    if (action === 'pin') {\n      Modal.confirm({\n        title: t('title', { keyPrefix: 'question_detail.pin' }),\n        content: t('content', { keyPrefix: 'question_detail.pin' }),\n        cancelBtnVariant: 'link',\n        confirmText: t('confirm_btn', { keyPrefix: 'question_detail.pin' }),\n        onConfirm: () => {\n          handleCommon(params);\n        },\n      });\n    } else {\n      handleCommon(params);\n    }\n  };\n\n  const handleAction = (action) => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    if (action === 'delete') {\n      handleDelete();\n    }\n\n    if (action === 'undelete') {\n      handleUndelete();\n    }\n\n    if (action === 'report') {\n      handleReport();\n    }\n\n    if (action === 'close') {\n      handleClose();\n    }\n\n    if (action === 'reopen') {\n      handleReopen();\n    }\n\n    if (\n      action === 'pin' ||\n      action === 'unpin' ||\n      action === 'hide' ||\n      action === 'show'\n    ) {\n      handlOtherActions(action);\n    }\n  };\n\n  const firstAction =\n    memberActions?.filter(\n      (v) =>\n        v.action === 'report' ||\n        v.action === 'edit' ||\n        v.action === 'delete' ||\n        v.action === 'undelete',\n    ) || [];\n  const secondAction =\n    memberActions?.filter(\n      (v) =>\n        v.action === 'close' ||\n        v.action === 'reopen' ||\n        v.action === 'pin' ||\n        v.action === 'unpin' ||\n        v.action === 'hide' ||\n        v.action === 'show',\n    ) || [];\n\n  return (\n    <>\n      <div className=\"md-show align-items-center\">\n        <Share\n          type={type}\n          qid={qid}\n          aid={aid}\n          title={title}\n          className=\"link-secondary small\"\n        />\n        {firstAction?.map((item) => {\n          if (item.action === 'edit') {\n            return (\n              <Link\n                key={item.action}\n                to={editUrl}\n                className=\"link-secondary p-0 small ms-3\"\n                onClick={(evt) => handleEdit(evt, editUrl)}\n                style={{ lineHeight: '23px' }}>\n                {item.name}\n              </Link>\n            );\n          }\n          return (\n            <Button\n              key={item.action}\n              variant=\"link\"\n              size=\"sm\"\n              className=\"link-secondary p-0 ms-3\"\n              onClick={() => handleAction(item.action)}>\n              {item.name}\n            </Button>\n          );\n        })}\n        {secondAction.length > 0 && (\n          <Dropdown className=\"ms-3 d-flex\">\n            <Dropdown.Toggle\n              variant=\"link\"\n              size=\"sm\"\n              title={t('action', { keyPrefix: 'question_detail' })}\n              className=\"link-secondary p-0 no-toggle\">\n              <Icon name=\"three-dots\" />\n            </Dropdown.Toggle>\n            <Dropdown.Menu>\n              {secondAction.map((item) => {\n                return (\n                  <Dropdown.Item\n                    key={item.action}\n                    onClick={() => handleAction(item.action)}>\n                    {item.name}\n                  </Dropdown.Item>\n                );\n              })}\n            </Dropdown.Menu>\n          </Dropdown>\n        )}\n      </div>\n      <div className=\"md-hide\">\n        {memberActions.length > 0 && (\n          <Dropdown className=\"d-flex\">\n            <Dropdown.Toggle\n              variant=\"link\"\n              size=\"sm\"\n              title={t('action', { keyPrefix: 'question_detail' })}\n              className=\"link-secondary no-toggle\">\n              <Icon name=\"three-dots\" />\n            </Dropdown.Toggle>\n            <Dropdown.Menu>\n              <Share\n                type={type}\n                qid={qid}\n                aid={aid}\n                title={title}\n                className=\"inherit\"\n                mode=\"mobile\"\n              />\n              {[...firstAction, ...secondAction].map((item) => {\n                return (\n                  <Dropdown.Item\n                    key={item.action}\n                    onClick={() => handleAction(item.action)}>\n                    {item.name}\n                  </Dropdown.Item>\n                );\n              })}\n            </Dropdown.Menu>\n          </Dropdown>\n        )}\n      </div>\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/PageTags/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useLayoutEffect } from 'react';\nimport { Helmet } from 'react-helmet-async';\n\nimport { REACT_BASE_PATH } from '@/router/alias';\nimport { brandingStore, pageTagStore, siteInfoStore } from '@/stores';\nimport { getCurrentLang } from '@/utils/localize';\n\nconst doInsertCustomCSS = !document.querySelector('link[href*=\"custom.css\"]');\n\nconst Index: FC = () => {\n  const { favicon, square_icon } = brandingStore((state) => state.branding);\n  const { pageTitle, keywords, description } = pageTagStore(\n    (state) => state.items,\n  );\n  const appVersion = siteInfoStore((_) => _.version);\n  const hashVersion = siteInfoStore((_) => _.revision);\n  const siteName = siteInfoStore((_) => _.siteInfo).name;\n  const setAppGenerator = () => {\n    if (!appVersion) {\n      return;\n    }\n    const generatorMetaNode = document.querySelector('meta[name=\"generator\"]');\n    if (generatorMetaNode) {\n      generatorMetaNode.setAttribute(\n        'content',\n        `Answer ${appVersion} - https://github.com/apache/answer version ${hashVersion}`,\n      );\n    }\n  };\n  const setDocTitle = () => {\n    try {\n      if (pageTitle) {\n        document.title = pageTitle;\n      }\n      // eslint-disable-next-line no-empty\n    } catch (ex) {}\n  };\n  const currentLang = getCurrentLang();\n  const setDocLang = () => {\n    if (currentLang) {\n      document.documentElement.setAttribute(\n        'lang',\n        currentLang.replace('_', '-'),\n      );\n    }\n  };\n  // properties used for social media tags\n  const openGraphType = 'website';\n  const twitterType = 'summary';\n  const { href } = window.location;\n  const { hostname } = new URL(href);\n\n  useEffect(() => {\n    setDocLang();\n  }, [currentLang]);\n  useEffect(() => {\n    setAppGenerator();\n  }, [appVersion]);\n  useLayoutEffect(() => {\n    setDocTitle();\n  }, [pageTitle]);\n  return (\n    <Helmet>\n      <link\n        rel=\"icon\"\n        type=\"image/png\"\n        href={favicon || square_icon || `${REACT_BASE_PATH}/favicon.ico`}\n      />\n      <link rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href={square_icon} />\n      <link rel=\"apple-touch-icon\" type=\"image/png\" href={square_icon} />\n      <title>{pageTitle}</title>\n      {keywords && <meta name=\"keywords\" content={keywords} />}\n      {description && <meta name=\"description\" content={description} />}\n      {doInsertCustomCSS && (\n        <link\n          rel=\"stylesheet\"\n          href={`${process.env.PUBLIC_URL}${REACT_BASE_PATH}/custom.css`}\n        />\n      )}\n      {/* Social media meta share tags start here */}\n      <meta property=\"og:type\" content={openGraphType} />\n      <meta property=\"og:title\" name=\"twitter:title\" content={pageTitle} />\n      <meta property=\"og:site_name\" content={siteName} />\n      <meta property=\"og:url\" content={href} />\n      {description && <meta property=\"og:description\" content={description} />}\n      <meta\n        property=\"og:image\"\n        itemProp=\"image primaryImageOfPage\"\n        content={square_icon || favicon || '/favicon.ico'}\n      />\n      <meta name=\"twitter:card\" content={twitterType} />\n      <meta name=\"twitter:domain\" content={hostname} />\n      {description && <meta name=\"twitter:description\" content={description} />}\n      <meta\n        name=\"twitter:image\"\n        content={square_icon || favicon || '/favicon.ico'}\n      />\n      {/* Social media meta share tags end here */}\n    </Helmet>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/Pagination/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Pagination } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams, useNavigate, useLocation } from 'react-router-dom';\n\nimport { floppyNavigation } from '@/utils';\n\ninterface Props {\n  currentPage: number;\n  pageSize: number;\n  totalSize: number;\n  pathname?: string;\n}\n\ninterface PageItemProps {\n  page: number;\n  currentPage: number;\n  path: string;\n}\n\nconst pageArr = [\n  {\n    href: '1',\n    page: 1,\n  },\n  {\n    href: '#!',\n    page: 2,\n  },\n  {\n    href: '#!',\n    page: 3,\n  },\n  {\n    href: '#!',\n    page: 4,\n  },\n  {\n    href: '#!',\n    page: 5,\n  },\n];\n\nconst PageItem = ({ page, currentPage, path }: PageItemProps) => {\n  const navigate = useNavigate();\n  return (\n    <Pagination.Item\n      active={currentPage === page}\n      href={path}\n      onClick={(e) => {\n        if (floppyNavigation.shouldProcessLinkClick(e)) {\n          e.preventDefault();\n          e.stopPropagation();\n          navigate(path);\n        }\n      }}>\n      {page}\n    </Pagination.Item>\n  );\n};\n\nconst Index: FC<Props> = ({\n  currentPage = 1,\n  pageSize = 15,\n  totalSize = 0,\n  pathname = '',\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'pagination' });\n  const location = useLocation();\n  if (!pathname) {\n    pathname = location.pathname;\n  }\n  const [searchParams] = useSearchParams();\n  const navigate = useNavigate();\n  const totalPage = Math.ceil(totalSize / pageSize);\n  const realPage = currentPage > totalPage ? totalPage : currentPage;\n\n  const mapPage = pageArr.filter((i) => i.page <= totalPage);\n\n  if (totalPage <= 1) {\n    return null;\n  }\n\n  const handleParams = (pageNum): string => {\n    searchParams.set('page', String(pageNum));\n    const searchStr = searchParams.toString();\n    return `${pathname}?${searchStr}`;\n  };\n  return (\n    <Pagination size=\"sm\" className=\"d-inline-flex mb-0\">\n      {currentPage > 1 && (\n        <Pagination.Prev\n          href={handleParams(currentPage - 1)}\n          onClick={(e) => {\n            if (floppyNavigation.shouldProcessLinkClick(e)) {\n              e.preventDefault();\n              navigate(handleParams(currentPage - 1));\n            }\n          }}>\n          {t('prev')}\n        </Pagination.Prev>\n      )}\n      {currentPage >= 1 && currentPage <= 4 && (\n        <>\n          {mapPage.map((item) => {\n            return (\n              <PageItem\n                key={item.page}\n                page={item.page}\n                currentPage={currentPage}\n                path={handleParams(item.page)}\n              />\n            );\n          })}\n        </>\n      )}\n      {currentPage === 4 && totalPage > 6 && (\n        <PageItem\n          key=\"page6\"\n          page={6}\n          currentPage={currentPage}\n          path={handleParams(6)}\n        />\n      )}\n\n      {currentPage > 4 && (\n        <>\n          <PageItem\n            key=\"first\"\n            page={1}\n            currentPage={currentPage}\n            path={handleParams(1)}\n          />\n\n          <Pagination.Ellipsis className=\"pe-none\" />\n        </>\n      )}\n      {currentPage >= 5 && (\n        <>\n          <PageItem\n            key={realPage - 2}\n            page={realPage - 2}\n            currentPage={currentPage}\n            path={handleParams(realPage - 2)}\n          />\n          <PageItem\n            key={realPage - 1}\n            page={realPage - 1}\n            currentPage={currentPage}\n            path={handleParams(realPage - 1)}\n          />\n        </>\n      )}\n\n      {currentPage > totalPage && (\n        <PageItem\n          key={realPage}\n          page={realPage}\n          currentPage={currentPage}\n          path={handleParams(realPage)}\n        />\n      )}\n\n      {currentPage >= 5 &&\n        totalPage >= currentPage &&\n        new Array(\n          totalPage <= 3\n            ? totalPage - currentPage + 1\n            : Math.min(totalPage - currentPage + 1, 3),\n        )\n          .fill('')\n          .map((v, i) => {\n            return (\n              <PageItem\n                key={`${currentPage + i}`}\n                page={currentPage + i}\n                currentPage={currentPage}\n                path={handleParams(currentPage + i)}\n              />\n            );\n          })}\n      {totalPage > 5 && realPage + 2 < totalPage && (\n        <Pagination.Ellipsis className=\"pe-none\" />\n      )}\n\n      {totalPage > 0 && currentPage < totalPage && (\n        <Pagination.Next\n          disabled={currentPage === totalPage}\n          href={handleParams(currentPage + 1)}\n          onClick={(e) => {\n            if (floppyNavigation.shouldProcessLinkClick(e)) {\n              e.preventDefault();\n              navigate(handleParams(currentPage + 1));\n            }\n          }}>\n          {t('next')}\n        </Pagination.Next>\n      )}\n    </Pagination>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/PinList/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { ListGroup, Stack } from 'react-bootstrap';\nimport { NavLink } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Counts } from '@/components';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface IProps {\n  data: any[];\n}\n\nconst PinList: FC<IProps> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  if (!data?.length) return null;\n\n  return (\n    <ListGroup.Item className=\"py-3 px-0 border-start-0 border-end-0\">\n      <Stack\n        direction=\"horizontal\"\n        gap={3}\n        className=\"overflow-x-auto align-items-stretch\">\n        {data.map((item) => {\n          return (\n            <ListGroup.Item\n              action\n              as=\"li\"\n              key={item.id}\n              className=\"border-0 p-0\"\n              style={{\n                minWidth: '238px',\n                width: `${100 / data.length}%`,\n              }}>\n              <NavLink\n                to={pathFactory.questionLanding(item.id, item.url_title)}\n                className=\"border rounded h-100 d-flex flex-column justify-content-between p-3\">\n                <h6 className=\"text-wrap link-dark text-break text-truncate-2\">\n                  {item.title}\n                  {item.status === 2 ? ` [${t('closed')}]` : ''}\n                </h6>\n\n                <Counts\n                  data={{\n                    votes: item.vote_count,\n                    answers: item.answer_count,\n                    views: item.view_count,\n                  }}\n                  isAccepted={item.accepted_answer_id >= 1}\n                  showViews={false}\n                  className=\"mt-2 mt-md-0 small text-secondary\"\n                />\n              </NavLink>\n            </ListGroup.Item>\n          );\n        })}\n      </Stack>\n    </ListGroup.Item>\n  );\n};\n\nexport default PinList;\n"
  },
  {
    "path": "ui/src/components/PluginRender/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, ReactNode, useEffect, useState } from 'react';\n\nimport PluginKit, { Plugin, PluginType } from '@/utils/pluginKit';\n\n// Marker component for plugin insertion point\nexport const PluginSlot: FC = () => null;\n/**\n * Note：Please set at least either of the `slug_name` and `type` attributes, otherwise no plugins will be rendered.\n *\n * @field slug_name: The `slug_name` of the plugin needs to be rendered.\n *                   If this property is set, `PluginRender` will use it first (regardless of whether `type` is set)\n *                   to find the corresponding plugin and render it.\n * @field type: Used to formulate the rendering of all plugins of this type.\n *              (if the `slug_name` attribute is set, it will be ignored)\n * @field prop: Any attribute you want to configure, e.g. `className`\n *\n * For editor type plugins, use <PluginSlot /> component as a marker to indicate where plugins should be inserted.\n */\n\ninterface Props {\n  slug_name?: string;\n  type: PluginType;\n  children?: ReactNode;\n  className?: string;\n  [key: string]: unknown;\n}\n\nconst Index: FC<Props> = ({\n  slug_name,\n  type,\n  children = null,\n  className,\n  ...props\n}) => {\n  const [pluginSlice, setPluginSlice] = useState<Plugin[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    let mounted = true;\n\n    const loadPlugins = async () => {\n      await PluginKit.initialization;\n\n      if (!mounted) return;\n\n      const plugins = PluginKit.getPlugins().filter(\n        (plugin) => plugin.activated,\n      );\n      console.log(\n        '[PluginRender] Loaded plugins:',\n        plugins.map((p) => p.info.slug_name),\n      );\n      const filtered: Plugin[] = [];\n\n      plugins.forEach((plugin) => {\n        if (type && slug_name) {\n          if (\n            plugin.info.slug_name === slug_name &&\n            plugin.info.type === type\n          ) {\n            filtered.push(plugin);\n          }\n        } else if (type) {\n          if (plugin.info.type === type) {\n            filtered.push(plugin);\n          }\n        } else if (slug_name) {\n          if (plugin.info.slug_name === slug_name) {\n            filtered.push(plugin);\n          }\n        }\n      });\n\n      if (mounted) {\n        setPluginSlice(filtered);\n        setIsLoading(false);\n      }\n    };\n\n    loadPlugins();\n\n    return () => {\n      mounted = false;\n    };\n  }, [slug_name, type]);\n\n  /**\n   * TODO: Rendering control for non-builtin plug-ins\n   * ps: Logic such as version compatibility determination can be placed here\n   */\n  if (isLoading) {\n    // Don't render anything while loading to avoid flashing\n    if (type === 'editor') {\n      return <div className={className}>{children}</div>;\n    }\n    return null;\n  }\n\n  if (pluginSlice.length === 0) {\n    if (type === 'editor') {\n      return <div className={className}>{children}</div>;\n    }\n    return null;\n  }\n\n  if (type === 'editor') {\n    // Use PluginSlot marker to insert plugins at the correct position\n    const nodes = React.Children.map(children, (child) => {\n      // Check if this is the PluginSlot marker\n      if (React.isValidElement(child) && child.type === PluginSlot) {\n        return (\n          <>\n            {pluginSlice.map((ps) => {\n              const PluginFC = ps.component as FC<typeof props>;\n              return <PluginFC key={ps.info.slug_name} {...props} />;\n            })}\n            {pluginSlice.length > 0 && <div className=\"toolbar-divider\" />}\n          </>\n        );\n      }\n      return child;\n    });\n\n    return <div className={className}>{nodes}</div>;\n  }\n\n  return (\n    <>\n      {pluginSlice.map((ps) => {\n        const PluginFC = ps.component as FC<\n          { className?: string } & typeof props\n        >;\n        return (\n          <PluginFC key={ps.info.slug_name} className={className} {...props} />\n        );\n      })}\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/QueryGroup/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.md-show {\n  display: flex !important;\n}\n.md-hide {\n  display: none;\n}\n\n@media screen and (max-width: 768px) {\n  .md-show {\n    display: none !important;\n  }\n  .md-hide {\n    display: block;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/QueryGroup/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ButtonGroup, Button, DropdownButton, Dropdown } from 'react-bootstrap';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { REACT_BASE_PATH } from '@/router/alias';\nimport { floppyNavigation } from '@/utils';\n\nimport './index.scss';\n\ninterface Props {\n  data;\n  i18nKeyPrefix: string;\n  currentSort: string;\n  sortKey?: string;\n  className?: string;\n  pathname?: string;\n  wrapClassName?: string;\n  maxBtnCount?: number;\n}\nconst Index: FC<Props> = ({\n  data = [],\n  currentSort = '',\n  sortKey = 'order',\n  i18nKeyPrefix = '',\n  className = '',\n  pathname = '',\n  wrapClassName = '',\n  maxBtnCount = 3,\n}) => {\n  const [searchParams, setUrlSearchParams] = useSearchParams();\n  const navigate = useNavigate();\n\n  const { t } = useTranslation('translation', {\n    keyPrefix: i18nKeyPrefix,\n  });\n\n  const handleParams = (order): string => {\n    searchParams.delete('page');\n    searchParams.set(sortKey, order);\n    const searchStr = searchParams.toString();\n    return `?${searchStr}`;\n  };\n\n  const handleClick = (e, type) => {\n    const str = handleParams(type);\n    if (floppyNavigation.shouldProcessLinkClick(e)) {\n      e.preventDefault();\n      if (pathname) {\n        navigate(`${pathname}${str}`);\n      } else {\n        setUrlSearchParams(str);\n      }\n    }\n  };\n  const moreBtnData = data.length > 4 ? data.slice(maxBtnCount) : [];\n  const normalBtnData = data.length > 4 ? data.slice(0, maxBtnCount) : data;\n  const currentBtn = moreBtnData.find((btn) => {\n    return (typeof btn === 'string' ? btn : btn.name) === currentSort;\n  });\n\n  return (\n    <>\n      <ButtonGroup size=\"sm\" className={classNames('md-show', wrapClassName)}>\n        {normalBtnData.map((btn) => {\n          const key = typeof btn === 'string' ? btn : btn.sort;\n          const name = typeof btn === 'string' ? btn : btn.name;\n          return (\n            <Button\n              key={key}\n              variant=\"outline-secondary\"\n              active={currentSort === name}\n              className={classNames('text-capitalize fit-content', className)}\n              href={\n                pathname\n                  ? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`\n                  : handleParams(key)\n              }\n              onClick={(evt) => handleClick(evt, key)}>\n              {t(name)}\n            </Button>\n          );\n        })}\n        {moreBtnData.length > 0 && (\n          <DropdownButton\n            size=\"sm\"\n            variant={currentBtn ? 'secondary' : 'outline-secondary'}\n            as={ButtonGroup}\n            title={currentBtn ? t(currentSort) : t('more')}>\n            {moreBtnData.map((btn) => {\n              const key = typeof btn === 'string' ? btn : btn.sort;\n              const name = typeof btn === 'string' ? btn : btn.name;\n              return (\n                <Dropdown.Item\n                  as=\"a\"\n                  key={key}\n                  active={currentSort === name}\n                  className={classNames('text-capitalize', className)}\n                  href={\n                    pathname\n                      ? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`\n                      : handleParams(key)\n                  }\n                  onClick={(evt) => handleClick(evt, key)}>\n                  {t(name)}\n                </Dropdown.Item>\n              );\n            })}\n          </DropdownButton>\n        )}\n      </ButtonGroup>\n      <DropdownButton\n        size=\"sm\"\n        variant=\"outline-secondary\"\n        className={classNames('md-hide', wrapClassName)}\n        title={t(currentSort)}>\n        {data.map((btn) => {\n          const key = typeof btn === 'string' ? btn : btn.sort;\n          const name = typeof btn === 'string' ? btn : btn.name;\n          return (\n            <Dropdown.Item\n              as=\"a\"\n              key={key}\n              active={currentSort === name}\n              className={classNames('text-capitalize', className)}\n              href={\n                pathname\n                  ? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`\n                  : handleParams(key)\n              }\n              onClick={(evt) => handleClick(evt, key)}>\n              {t(name)}\n            </Dropdown.Item>\n          );\n        })}\n      </DropdownButton>\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/QuestionList/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { ListGroup, Dropdown } from 'react-bootstrap';\nimport { NavLink, useSearchParams, useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport {\n  Tag,\n  Pagination,\n  FormatTime,\n  Empty,\n  BaseUserCard,\n  QueryGroup,\n  QuestionListLoader,\n  Counts,\n  PinList,\n  Icon,\n} from '@/components';\nimport * as Type from '@/common/interface';\nimport { useSkeletonControl } from '@/hooks';\nimport Storage from '@/utils/storage';\nimport { LIST_VIEW_STORAGE_KEY } from '@/common/constants';\n\nexport const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [\n  'newest',\n  'active',\n  'unanswered',\n  'recommend',\n  'frequent',\n  'score',\n];\ninterface Props {\n  source: 'questions' | 'tag' | 'linked';\n  order?: Type.QuestionOrderBy;\n  data;\n  orderList?: Type.QuestionOrderBy[];\n  isLoading: boolean;\n}\n\nconst QuestionList: FC<Props> = ({\n  source,\n  order,\n  data,\n  orderList,\n  isLoading = false,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  const navigate = useNavigate();\n  const [urlSearchParams] = useSearchParams();\n  const { isSkeletonShow } = useSkeletonControl(isLoading);\n  const curOrder =\n    order || urlSearchParams.get('order') || QUESTION_ORDER_KEYS[0];\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const pageSize = 20;\n  const count = data?.count || 0;\n  const orderKeys = orderList || QUESTION_ORDER_KEYS;\n  const pinData =\n    source === 'questions'\n      ? data?.list?.filter((v) => v.pin === 2).slice(0, 3)\n      : [];\n  const renderData = data?.list?.filter(\n    (v) => pinData.findIndex((p) => p.id === v.id) === -1,\n  );\n\n  const [viewType, setViewType] = useState('card');\n\n  const handleViewMode = (key) => {\n    Storage.set(LIST_VIEW_STORAGE_KEY, key);\n    setViewType(key);\n  };\n\n  const handleNavigate = (href) => {\n    navigate(href);\n  };\n\n  useEffect(() => {\n    const type = Storage.get(LIST_VIEW_STORAGE_KEY) || 'card';\n    setViewType(type);\n  }, []);\n\n  return (\n    <div>\n      <div className=\"mb-3 d-flex flex-wrap justify-content-between\">\n        <h5 className=\"fs-5 text-nowrap mb-3 mb-md-0\">\n          {source === 'questions'\n            ? t('all_questions')\n            : source === 'linked'\n              ? t('x_posts', { count })\n              : t('x_questions', { count })}\n        </h5>\n        <div className=\"d-flex flex-wrap\">\n          <QueryGroup\n            data={orderKeys}\n            currentSort={curOrder}\n            pathname={source === 'questions' ? '/questions' : ''}\n            i18nKeyPrefix=\"question\"\n            maxBtnCount={source === 'tag' ? 3 : 4}\n            wrapClassName=\"me-2\"\n          />\n          <Dropdown align=\"end\" onSelect={handleViewMode}>\n            <Dropdown.Toggle variant=\"outline-secondary\" size=\"sm\">\n              <Icon name={viewType === 'card' ? 'view-stacked' : 'list'} />\n            </Dropdown.Toggle>\n\n            <Dropdown.Menu>\n              <Dropdown.Header as=\"h6\">\n                {t('view', { keyPrefix: 'btns' })}\n              </Dropdown.Header>\n              <Dropdown.Item eventKey=\"card\" active={viewType === 'card'}>\n                {t('card', { keyPrefix: 'btns' })}\n              </Dropdown.Item>\n              <Dropdown.Item eventKey=\"compact\" active={viewType === 'compact'}>\n                {t('compact', { keyPrefix: 'btns' })}\n              </Dropdown.Item>\n            </Dropdown.Menu>\n          </Dropdown>\n        </div>\n      </div>\n      <ListGroup className=\"rounded-0\">\n        {isSkeletonShow ? (\n          <QuestionListLoader />\n        ) : (\n          <>\n            <PinList data={pinData} />\n            {renderData?.map((li) => {\n              return (\n                <ListGroup.Item\n                  key={li.id}\n                  action\n                  as=\"li\"\n                  onClick={() =>\n                    handleNavigate(\n                      pathFactory.questionLanding(li.id, li.url_title),\n                    )\n                  }\n                  className=\"py-3 px-2 border-start-0 border-end-0 position-relative pointer\">\n                  <div className=\"d-flex flex-wrap text-secondary small mb-12\">\n                    <BaseUserCard\n                      data={li.operator}\n                      className=\"me-1\"\n                      avatarClass=\"me-1\"\n                    />\n                    •\n                    <FormatTime\n                      time={\n                        curOrder === 'active' ? li.operated_at : li.created_at\n                      }\n                      className=\"text-secondary ms-1 flex-shrink-0\"\n                    />\n                  </div>\n                  <h5 className=\"text-wrap text-break\">\n                    <NavLink\n                      className=\"link-dark d-block\"\n                      onClick={(e) => e.stopPropagation()}\n                      to={pathFactory.questionLanding(li.id, li.url_title)}>\n                      {li.title}\n                      {li.status === 2 ? ` [${t('closed')}]` : ''}\n                    </NavLink>\n                  </h5>\n                  {viewType === 'card' && (\n                    <div className=\"text-truncate-2 mb-2\">\n                      <NavLink\n                        to={pathFactory.questionLanding(li.id, li.url_title)}\n                        className=\"d-block small text-body\"\n                        dangerouslySetInnerHTML={{ __html: li.description }}\n                        onClick={(e) => e.stopPropagation()}\n                      />\n                    </div>\n                  )}\n\n                  <div className=\"question-tags mb-12\">\n                    {Array.isArray(li.tags)\n                      ? li.tags.map((tag, index) => {\n                          return (\n                            <Tag\n                              key={tag.slug_name}\n                              className={`${\n                                li.tags.length - 1 === index ? '' : 'me-1'\n                              }`}\n                              data={tag}\n                            />\n                          );\n                        })\n                      : null}\n                  </div>\n                  <div className=\"small text-secondary\">\n                    <Counts\n                      data={{\n                        votes: li.vote_count,\n                        answers: li.answer_count,\n                        views: li.view_count,\n                      }}\n                      isAccepted={li.accepted_answer_id >= 1}\n                      className=\"mt-2 mt-md-0\"\n                    />\n                  </div>\n                </ListGroup.Item>\n              );\n            })}\n          </>\n        )}\n      </ListGroup>\n      {count <= 0 && !isLoading && <Empty />}\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={count}\n          pageSize={pageSize}\n          pathname={source === 'questions' ? '/questions' : ''}\n        />\n      </div>\n    </div>\n  );\n};\nexport default QuestionList;\n"
  },
  {
    "path": "ui/src/components/QuestionListLoader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroupItem } from 'react-bootstrap';\n\ninterface Props {\n  count?: number;\n}\n\nconst Index: FC<Props> = ({ count = 10 }) => {\n  const list = new Array(count).fill(0).map((v, i) => v + i);\n  return (\n    <>\n      {list.map((v) => (\n        <ListGroupItem\n          className=\"bg-transparent py-3 px-2 border-start-0 border-end-0 placeholder-glow\"\n          key={v}>\n          <div\n            className=\"placeholder h5 align-top d-block\"\n            style={{ height: '21px', width: '35%' }}\n          />\n\n          <div\n            className=\"placeholder w-75 h5 align-top\"\n            style={{ height: '24px' }}\n          />\n\n          <div\n            className=\"placeholder w-100 d-block align-top mb-2\"\n            style={{ height: '21px' }}\n          />\n          <div\n            className=\"placeholder w-100 d-block align-top mb-2\"\n            style={{ height: '21px' }}\n          />\n\n          <div\n            className=\"placeholder w-50 align-top mb-12\"\n            style={{ height: '24px' }}\n          />\n\n          <div\n            className=\"placeholder w-25 align-top d-block\"\n            style={{ height: '21px' }}\n          />\n        </ListGroupItem>\n      ))}\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/SchemaForm/README.md",
    "content": "# Schema Form\n\n## Introduction\n\nA React component capable of building HTML forms out of a [JSON schema](https://json-schema.org/understanding-json-schema/index.html).\n\n## Usage\n\n```tsx\nimport React, { useState } from 'react';\n\nimport {\n  SchemaForm,\n  initFormData,\n  JSONSchema,\n  UISchema,\n  FormKit,\n} from '@/components';\n\nconst schema: JSONSchema = {\n  title: 'General',\n  properties: {\n    name: {\n      type: 'string',\n      title: 'Name',\n    },\n    age: {\n      type: 'number',\n      title: 'Age',\n    },\n    sex: {\n      type: 'string',\n      title: 'sex',\n      enum: [1, 2],\n      enumNames: ['male', 'female'],\n    },\n  },\n};\n\nconst uiSchema: UISchema = {\n  name: {\n    'ui:widget': 'input',\n  },\n  age: {\n    'ui:widget': 'input',\n    'ui:options': {\n      type: 'number',\n    },\n  },\n  sex: {\n    'ui:widget': 'radio',\n  },\n};\n\nconst Form = () => {\n  const [formData, setFormData] = useState(initFormData(schema));\n\n  const formRef = useRef<{\n    validator: () => Promise<boolean>;\n  }>(null);\n\n  const refreshConfig: FormKit['refreshConfig'] = async () => {\n    // refreshFormConfig();\n  };\n\n  const handleChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <SchemaForm\n      ref={formRef}\n      schema={schema}\n      uiSchema={uiSchema}\n      formData={formData}\n      onChange={handleChange}\n      refreshConfig={refreshConfig}\n    />\n  );\n};\n\nexport default Form;\n```\n\n---\n\n## Form Props\n\n```ts\ninterface FormProps {\n  // Describe the form structure with schema\n  schema: JSONSchema | null;\n  // Describe the properties of the field\n  uiSchema?: UISchema;\n  // Describe form data\n  formData: Type.FormDataType | null;\n  // Callback function when form data changes\n  onChange?: (data: Type.FormDataType) => void;\n  // Handler for when a form fires a `submit` event\n  onSubmit?: (e: React.FormEvent) => void;\n  /**\n   * Callback method for updating form configuration\n   * information (schema/uiSchema) in UIAction\n   */\n  refreshConfig?: FormKit['refreshConfig'];\n}\n```\n\n## Form Ref\n\n```ts\nexport interface FormRef {\n  validator: () => Promise<boolean>;\n}\n```\n\nWhen you need to validate a form and get the result outside the form, you can create a `FormRef` with `useRef` and pass it to the form using the `ref` property.\n\nThis allows you to validate the form and get the result outside the form using `formRef.current.validator()`.\n\n---\n\n## Types Definition\n\n### JSONSchema\n\n```ts\nexport interface JSONSchema {\n  title: string;\n  description?: string;\n  required?: string[];\n  properties: {\n    [key: string]: {\n      type: 'string' | 'boolean' | 'number';\n      title: string;\n      label?: string;\n      description?: string;\n      enum?: Array<string | boolean | number>;\n      enumNames?: string[];\n      default?: string | boolean | number;\n    };\n  };\n}\n```\n\n### UISchema\n\n```ts\nexport interface UISchema {\n  [key: string]: {\n    'ui:widget'?: UIWidget;\n    'ui:options'?: UIOptions;\n  };\n}\n```\n\n### UIWidget\n\n```ts\nexport type UIWidget =\n  | 'textarea'\n  | 'input'\n  | 'checkbox'\n  | 'radio'\n  | 'select'\n  | 'upload'\n  | 'timezone'\n  | 'switch'\n  | 'legend'\n  | 'button';\n```\n\n---\n\n### UIOptions\n\n```ts\nexport type UIOptions =\n  | InputOptions\n  | SelectOptions\n  | UploadOptions\n  | SwitchOptions\n  | TimezoneOptions\n  | CheckboxOptions\n  | RadioOptions\n  | TextareaOptions\n  | ButtonOptions;\n```\n\n#### BaseUIOptions\n\n```ts\nexport interface BaseUIOptions {\n  empty?: string;\n  // Will be appended to the className of the form component itself\n  className?: classnames.Argument;\n  class_name?: classnames.Argument;\n  // The className that will be attached to a form field container\n  field_class_name?: classnames.Argument;\n  // Make a form component render into simplified mode\n  readOnly?: boolean;\n  simplify?: boolean;\n  validator?: (\n    value,\n    formData?,\n  ) => Promise<string | true | void> | true | string;\n}\n```\n\n#### InputOptions\n\n```ts\nexport interface InputOptions extends BaseUIOptions {\n  placeholder?: string;\n  inputType?:\n    | 'color'\n    | 'date'\n    | 'datetime-local'\n    | 'email'\n    | 'month'\n    | 'number'\n    | 'password'\n    | 'range'\n    | 'search'\n    | 'tel'\n    | 'text'\n    | 'time'\n    | 'url'\n    | 'week';\n}\n```\n\n#### SelectOptions\n\n```ts\nexport interface SelectOptions extends UIOptions {}\n```\n\n#### UploadOptions\n\n```ts\nexport interface UploadOptions extends BaseUIOptions {\n  acceptType?: string;\n  imageType?: Type.UploadType;\n}\n```\n\n#### SwitchOptions\n\n```ts\nexport interface SwitchOptions extends BaseUIOptions {\n  label?: string;\n}\n```\n\n#### TimezoneOptions\n\n```ts\nexport interface TimezoneOptions extends UIOptions {\n  placeholder?: string;\n}\n```\n\n#### CheckboxOptions\n\n```ts\nexport interface CheckboxOptions extends UIOptions {}\n```\n\n#### RadioOptions\n\n```ts\nexport interface RadioOptions extends UIOptions {}\n```\n\n#### TextareaOptions\n\n```ts\nexport interface TextareaOptions extends UIOptions {\n  placeholder?: string;\n  rows?: number;\n}\n```\n\n#### ButtonOptions\n\n```ts\nexport interface ButtonOptions extends BaseUIOptions {\n  text: string;\n  icon?: string;\n  action?: UIAction;\n  variant?: ButtonProps['variant'];\n  size?: ButtonProps['size'];\n}\n```\n\n#### UIAction\n\n```ts\nexport interface UIAction {\n  url: string;\n  method?: 'get' | 'post' | 'put' | 'delete';\n  loading?: {\n    text: string;\n    state?: 'none' | 'pending' | 'completed';\n  };\n  on_complete?: {\n    toast_return_message?: boolean;\n    refresh_form_config?: boolean;\n  };\n}\n```\n\n#### FormKit\n\n```ts\nexport interface FormKit {\n  refreshConfig(): void;\n}\n```\n\n---\n\n### FormData\n\n```ts\nexport interface FormValue<T = any> {\n  value: T;\n  isInvalid: boolean;\n  errorMsg: string;\n  [prop: string]: any;\n}\n\nexport interface FormDataType {\n  [prop: string]: FormValue;\n}\n```\n\n---\n\n## Backend API\n\nFor backend generating modal form you can return json like this.\n\n### Response\n\n```json\n{\n  \"name\": \"string\",\n  \"slug_name\": \"string\",\n  \"description\": \"string\",\n  \"version\": \"string\",\n  \"config_fields\": [\n    {\n      \"name\": \"string\",\n      \"type\": \"textarea\",\n      \"title\": \"string\",\n      \"description\": \"string\",\n      \"required\": true,\n      \"value\": \"string\",\n      \"ui_options\": {\n        \"placeholder\": \"placeholder\",\n        \"rows\": 4\n      },\n      \"options\": [\n        {\n          \"value\": \"string\",\n          \"label\": \"string\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n## reference\n\n- [json schema](https://json-schema.org/understanding-json-schema/index.html)\n- [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form)\n- [vue-json-schema-form](https://github.com/lljj-x/vue-json-schema-form/)\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Button.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Button, ButtonProps, Spinner } from 'react-bootstrap';\n\nimport { request } from '@/utils';\nimport type { UIAction, FormKit } from '../types';\nimport { useToast } from '@/hooks';\nimport { Icon } from '@/components';\n\ninterface Props {\n  fieldName: string;\n  text: string;\n  action: UIAction | undefined;\n  actionType?: 'submit' | 'click';\n  clickCallback?: () => void;\n  formKit: FormKit;\n  readOnly: boolean;\n  variant?: ButtonProps['variant'];\n  size?: ButtonProps['size'];\n  iconName?: string;\n  nowrap?: boolean;\n  title?: string;\n}\nconst Index: FC<Props> = ({\n  fieldName,\n  action,\n  actionType = 'submit',\n  formKit,\n  text = '',\n  readOnly = false,\n  variant = 'primary',\n  size,\n  iconName = '',\n  nowrap = false,\n  clickCallback,\n  title,\n}) => {\n  const Toast = useToast();\n  const [isLoading, setLoading] = useState(false);\n  const handleToast = (msg, type: 'success' | 'danger' = 'success') => {\n    const tm = action?.on_complete?.toast_return_message;\n    if (tm === false || !msg) {\n      return;\n    }\n    Toast.onShow({\n      msg,\n      variant: type,\n    });\n  };\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const handleCallback = (resp) => {\n    const callback = action?.on_complete;\n    if (callback?.refresh_form_config) {\n      formKit.refreshConfig();\n    }\n  };\n  const handleAction = () => {\n    if (actionType === 'click') {\n      if (typeof clickCallback === 'function') {\n        clickCallback();\n      }\n      return;\n    }\n    if (!action) {\n      return;\n    }\n    setLoading(true);\n    request\n      .request({\n        method: action.method,\n        url: action.url,\n        timeout: 0,\n      })\n      .then((resp) => {\n        if ('message' in resp) {\n          handleToast(resp.message, 'success');\n        }\n        handleCallback(resp);\n      })\n      .catch((ex) => {\n        if (ex && 'msg' in ex) {\n          handleToast(ex.msg, 'danger');\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n  useEffect(() => {\n    if (action?.loading?.state === 'pending') {\n      setLoading(true);\n    }\n  }, []);\n  const loadingText = action?.loading?.text || text;\n  const disabled = isLoading || readOnly;\n  if (nowrap) {\n    return (\n      <Button\n        name={fieldName}\n        onClick={handleAction}\n        disabled={disabled}\n        size={size}\n        title={title}\n        variant={variant}>\n        {isLoading ? (\n          <>\n            <Spinner\n              className=\"align-middle me-2\"\n              animation=\"border\"\n              size=\"sm\"\n              variant={variant}\n            />\n            {loadingText}\n          </>\n        ) : (\n          text\n        )}\n        {iconName && <Icon name={iconName} />}\n      </Button>\n    );\n  }\n\n  return (\n    <div className=\"d-flex\">\n      <Button\n        name={fieldName}\n        onClick={handleAction}\n        disabled={disabled}\n        size={size}\n        title={title}\n        variant={variant}>\n        {isLoading ? (\n          <>\n            <Spinner\n              className=\"align-middle me-2\"\n              animation=\"border\"\n              size=\"sm\"\n              variant={variant}\n            />\n            {loadingText}\n          </>\n        ) : (\n          text\n        )}\n        {iconName && <Icon name={iconName} />}\n      </Button>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Check.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form, Stack } from 'react-bootstrap';\n\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  type: 'radio' | 'checkbox';\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  enumValues: (string | boolean | number)[];\n  enumNames: string[];\n  formData: Type.FormDataType;\n  readOnly?: boolean;\n}\nconst Index: FC<Props> = ({\n  type = 'radio',\n  fieldName,\n  onChange,\n  enumValues,\n  enumNames,\n  formData,\n  readOnly = false,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleCheck = (\n    evt: React.ChangeEvent<HTMLInputElement>,\n    index: number,\n  ) => {\n    const { name, checked } = evt.currentTarget;\n    enumValues[index] = checked;\n\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value: enumValues,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n  return (\n    <Stack direction=\"horizontal\">\n      {enumValues?.map((item, index) => {\n        return (\n          <Form.Check\n            key={String(item)}\n            inline\n            type={type}\n            name={fieldName}\n            id={`${fieldName}-${enumNames?.[index]}`}\n            label={enumNames?.[index]}\n            checked={fieldObject?.value?.[index] || false}\n            feedback={fieldObject?.errorMsg}\n            feedbackType=\"invalid\"\n            isInvalid={fieldObject?.isInvalid}\n            disabled={readOnly}\n            onChange={(evt) => handleCheck(evt, index)}\n          />\n        );\n      })}\n    </Stack>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Input.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  type: string | undefined;\n  placeholder: string | undefined;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n  readOnly: boolean;\n  min?: number;\n  max?: number;\n  inputMode?:\n    | 'text'\n    | 'search'\n    | 'none'\n    | 'tel'\n    | 'url'\n    | 'email'\n    | 'numeric'\n    | 'decimal'\n    | undefined;\n}\nconst Index: FC<Props> = ({\n  type = 'text',\n  placeholder = '',\n  fieldName,\n  onChange,\n  formData,\n  readOnly = false,\n  min = 0,\n  max,\n  inputMode = 'text',\n}) => {\n  const fieldObject = formData[fieldName];\n  const numberInputProps =\n    type === 'number'\n      ? { min, ...(max != null && max > 0 ? { max } : {}) }\n      : {};\n  const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {\n    const { name, value } = evt.currentTarget;\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value: type === 'number' ? Number(value) : value,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n\n  // For number type, use ?? to preserve 0 value; for other types, use || for backward compatibility\n  const inputValue =\n    type === 'number' ? (fieldObject?.value ?? '') : fieldObject?.value || '';\n\n  return (\n    <Form.Control\n      name={fieldName}\n      placeholder={placeholder}\n      type={type}\n      value={inputValue}\n      {...numberInputProps}\n      inputMode={inputMode}\n      onChange={handleChange}\n      disabled={readOnly}\n      isInvalid={fieldObject?.isInvalid}\n      style={type === 'color' ? { width: '100px', flex: 'none' } : {}}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/InputGroup.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { InputGroup } from 'react-bootstrap';\n\nimport type { FormKit, InputGroupOptions } from '../types';\n\nimport Button from './Button';\n\ninterface Props {\n  formKitWithContext: FormKit;\n  uiOpt: InputGroupOptions;\n  prefixText?: string;\n  suffixText?: string;\n  children: React.ReactNode;\n}\n\nconst InputGroupBtn = ({\n  formKitWithContext,\n  uiOpt,\n}: {\n  formKitWithContext: FormKit;\n  uiOpt:\n    | InputGroupOptions['prefixBtnOptions']\n    | InputGroupOptions['suffixBtnOptions'];\n}) => {\n  return (\n    <Button\n      fieldName=\"1\"\n      text={String(uiOpt?.text)}\n      iconName={uiOpt?.iconName ? uiOpt?.iconName : ''}\n      action={uiOpt?.action ? uiOpt?.action : undefined}\n      actionType=\"click\"\n      clickCallback={uiOpt?.clickCallback ? uiOpt?.clickCallback : undefined}\n      formKit={formKitWithContext}\n      variant={uiOpt?.variant ? uiOpt.variant : undefined}\n      size={uiOpt?.size ? uiOpt?.size : undefined}\n      title={uiOpt?.title ? uiOpt?.title : ''}\n      nowrap\n      readOnly={false}\n    />\n  );\n};\n\nconst Index: FC<Props> = ({\n  formKitWithContext,\n  uiOpt,\n  prefixText = null,\n  suffixText = null,\n  children,\n}) => {\n  return (\n    <InputGroup>\n      {prefixText && <InputGroup.Text>{prefixText}</InputGroup.Text>}\n      {uiOpt && 'prefixBtnOptions' in uiOpt && (\n        <InputGroupBtn\n          uiOpt={uiOpt.prefixBtnOptions}\n          formKitWithContext={formKitWithContext}\n        />\n      )}\n      {children}\n      {uiOpt && 'suffixBtnOptions' in uiOpt && (\n        <InputGroupBtn\n          uiOpt={uiOpt.suffixBtnOptions}\n          formKitWithContext={formKitWithContext}\n        />\n      )}\n      {suffixText ? <InputGroup.Text>{suffixText}</InputGroup.Text> : null}\n    </InputGroup>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Legend.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\ninterface Props {\n  title: string;\n  className?: string | undefined;\n}\nconst Index: FC<Props> = ({ title, className }) => {\n  return <Form.Label className={className}>{title}</Form.Label>;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Select.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  desc: string | undefined;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  enumValues: (string | boolean | number)[];\n  enumNames: string[];\n  formData: Type.FormDataType;\n  readOnly: boolean;\n}\nconst Index: FC<Props> = ({\n  desc,\n  fieldName,\n  onChange,\n  enumValues,\n  enumNames,\n  formData,\n  readOnly = false,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {\n    const { name, value } = evt.currentTarget;\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n  return (\n    <Form.Select\n      aria-label={desc}\n      name={fieldName}\n      value={fieldObject?.value || ''}\n      onChange={handleChange}\n      disabled={readOnly}\n      isInvalid={fieldObject?.isInvalid}>\n      {enumValues?.map((item, index) => {\n        return (\n          <option value={String(item)} key={String(item)}>\n            {enumNames?.[index]}\n          </option>\n        );\n      })}\n    </Form.Select>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Switch.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  label: string | undefined;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n  readOnly?: boolean;\n}\nconst Index: FC<Props> = ({\n  fieldName,\n  onChange,\n  label,\n  formData,\n  readOnly = false,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {\n    const { name, checked } = evt.currentTarget;\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value: checked,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n\n  return (\n    <Form.Check\n      name={fieldName}\n      type=\"switch\"\n      label={label}\n      checked={fieldObject?.value || ''}\n      feedback={fieldObject?.errorMsg}\n      feedbackType=\"invalid\"\n      isInvalid={fieldObject?.isInvalid}\n      disabled={readOnly}\n      onChange={handleChange}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/TagSelector.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\n\nimport { TagSelector } from '@/components';\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  maxTagLength?: number;\n  description?: string;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n}\nconst Index: FC<Props> = ({\n  description,\n  maxTagLength,\n  fieldName,\n  onChange,\n  formData,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (data: Type.Tag[]) => {\n    const state = {\n      ...formData,\n      [fieldName]: {\n        ...formData[fieldName],\n        value: data,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n\n  return (\n    <TagSelector\n      value={fieldObject?.value || []}\n      onChange={handleChange}\n      maxTagLength={maxTagLength || 0}\n      isInvalid={fieldObject?.isInvalid}\n      formText={description}\n      errMsg={fieldObject?.errorMsg}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Textarea.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\nimport classnames from 'classnames';\n\nimport type * as Type from '@/common/interface';\n\ninterface Props {\n  placeholder: string | undefined;\n  rows: number | undefined;\n  className: classnames.Argument;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n  readOnly: boolean;\n}\nconst Index: FC<Props> = ({\n  placeholder = '',\n  rows = 3,\n  className,\n  fieldName,\n  onChange,\n  formData,\n  readOnly = false,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {\n    const { name, value } = evt.currentTarget;\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n\n  return (\n    <Form.Control\n      as=\"textarea\"\n      name={fieldName}\n      placeholder={placeholder}\n      value={fieldObject?.value || ''}\n      onChange={handleChange}\n      isInvalid={fieldObject?.isInvalid}\n      rows={rows}\n      disabled={readOnly}\n      className={classnames(className)}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Timezone.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\n\nimport type * as Type from '@/common/interface';\nimport TimeZonePicker from '@/components/TimeZonePicker';\n\ninterface Props {\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n  readOnly?: boolean;\n}\nconst Index: FC<Props> = ({\n  fieldName,\n  onChange,\n  formData,\n  readOnly = false,\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {\n    const { name, value } = evt.currentTarget;\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value,\n        isInvalid: false,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n  return (\n    <TimeZonePicker\n      value={fieldObject?.value || ''}\n      isInvalid={fieldObject?.isInvalid}\n      name={fieldName}\n      disabled={readOnly}\n      onChange={handleChange}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/Upload.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Form } from 'react-bootstrap';\n\nimport classNames from 'classnames';\n\nimport type * as Type from '@/common/interface';\nimport BrandUpload from '@/components/BrandUpload';\n\ninterface Props {\n  type: Type.UploadType | undefined;\n  acceptType: string | undefined;\n  fieldName: string;\n  onChange?: (fd: Type.FormDataType) => void;\n  formData: Type.FormDataType;\n  readOnly?: boolean;\n  imgClassNames?: classNames.Argument;\n}\nconst Index: FC<Props> = ({\n  type = 'avatar',\n  acceptType = '',\n  fieldName,\n  onChange,\n  formData,\n  readOnly = false,\n  imgClassNames = '',\n}) => {\n  const fieldObject = formData[fieldName];\n  const handleChange = (name: string, value: string) => {\n    const state = {\n      ...formData,\n      [name]: {\n        ...formData[name],\n        value,\n      },\n    };\n    if (typeof onChange === 'function') {\n      onChange(state);\n    }\n  };\n  return (\n    <>\n      <BrandUpload\n        type={type}\n        acceptType={acceptType}\n        value={fieldObject?.value}\n        readOnly={readOnly}\n        onChange={(value) => handleChange(fieldName, value)}\n        imgClassNames={imgClassNames}\n      />\n      <Form.Control\n        name={fieldName}\n        className=\"d-none\"\n        isInvalid={fieldObject?.isInvalid}\n      />\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/SchemaForm/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Legend from './Legend';\nimport Select from './Select';\nimport Check from './Check';\nimport Switch from './Switch';\nimport Timezone from './Timezone';\nimport Upload from './Upload';\nimport Textarea from './Textarea';\nimport Input from './Input';\nimport Button from './Button';\nimport InputGroup from './InputGroup';\nimport TagSelector from './TagSelector';\n\nexport {\n  Legend,\n  Select,\n  Check,\n  Switch,\n  Timezone,\n  Upload,\n  Textarea,\n  Input,\n  Button,\n  InputGroup,\n  TagSelector,\n};\n"
  },
  {
    "path": "ui/src/components/SchemaForm/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, {\n  ForwardRefRenderFunction,\n  forwardRef,\n  useImperativeHandle,\n  useEffect,\n} from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport isEmpty from 'lodash/isEmpty';\nimport classnames from 'classnames';\n\nimport { scrollElementIntoView } from '@/utils';\nimport type * as Type from '@/common/interface';\n\nimport type {\n  JSONSchema,\n  FormProps,\n  FormRef,\n  BaseUIOptions,\n  FormKit,\n  InputGroupOptions,\n} from './types';\nimport {\n  Legend,\n  Select,\n  Check,\n  Switch,\n  Timezone,\n  Upload,\n  Textarea,\n  Input,\n  Button as SfButton,\n  InputGroup,\n  TagSelector,\n} from './components';\n\nexport * from './types';\n\n/**\n * TODO:\n *  - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.\n *  - Normalize and document `formData[key].hidden && 'd-none'`\n *  - Normalize and document `hiddenSubmit`\n *  - Improving field hints for `formData`\n *  - Optimise form data updates\n *    * Automatic field type conversion\n *    * Dynamic field generation\n */\n\n/**\n * json schema form\n * @param schema json schema\n * @param uiSchema ui schema\n * @param formData form data\n * @param onChange change event\n * @param onSubmit submit event\n */\nconst SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (\n  {\n    schema,\n    uiSchema = {},\n    refreshConfig,\n    formData,\n    onChange,\n    onSubmit,\n    hiddenSubmit = false,\n  },\n  ref,\n) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'form',\n  });\n  const { required = [], properties = {} } = schema || {};\n  // check required field\n  const excludes = required.filter((key) => !properties[key]);\n  if (excludes.length > 0) {\n    console.error(t('not_found_props', { key: excludes.join(', ') }));\n  }\n  formData ||= {};\n  const keys = Object.keys(properties);\n  /**\n   * Prevent components such as `select` from having default values,\n   * which are not generated on `formData`\n   */\n  const setDefaultValueAsDomBehaviour = () => {\n    keys.forEach((k) => {\n      const fieldVal = formData![k]?.value;\n      const metaProp = properties[k];\n      const uiCtrl = uiSchema[k]?.['ui:widget'];\n      if (!metaProp || !uiCtrl || fieldVal !== undefined) {\n        return;\n      }\n      if (uiCtrl === 'select' && metaProp.enum?.[0] !== undefined) {\n        formData![k] = {\n          errorMsg: '',\n          isInvalid: false,\n          value: metaProp.enum?.[0],\n        };\n      }\n    });\n  };\n  useEffect(() => {\n    setDefaultValueAsDomBehaviour();\n  }, [formData]);\n\n  const formKitWithContext: FormKit = {\n    refreshConfig() {\n      if (typeof refreshConfig === 'function') {\n        refreshConfig();\n      }\n    },\n  };\n\n  /**\n   * Form validation\n   * - Currently only dynamic forms are in use, the business form validation has been handed over to the server\n   */\n  const requiredValidator = () => {\n    const errors: string[] = [];\n    required.forEach((key) => {\n      if (!formData![key] || !formData![key].value) {\n        errors.push(key);\n      }\n    });\n    return errors;\n  };\n\n  const syncValidator = () => {\n    const errors: Array<{ key: string; msg: string }> = [];\n    const promises: Array<{\n      key: string;\n      promise;\n    }> = [];\n    keys.forEach((key) => {\n      const { validator } = uiSchema[key]?.['ui:options'] || {};\n      if (validator instanceof Function) {\n        const value = formData![key]?.value;\n        promises.push({\n          key,\n          promise: validator(value, formData),\n        });\n      }\n    });\n    return Promise.allSettled(promises.map((item) => item.promise)).then(\n      (results) => {\n        results.forEach((result, index) => {\n          const { key } = promises[index];\n          if (result.status === 'rejected') {\n            errors.push({\n              key,\n              msg: result.reason.message,\n            });\n          }\n\n          if (result.status === 'fulfilled') {\n            const msg = result.value;\n            if (typeof msg === 'string') {\n              errors.push({\n                key,\n                msg,\n              });\n            }\n          }\n        });\n        return errors;\n      },\n    );\n  };\n\n  const validator = async (): Promise<boolean> => {\n    const errors = requiredValidator();\n    if (errors.length > 0) {\n      formData = errors.reduce((acc, cur) => {\n        acc[cur] = {\n          ...formData![cur],\n          isInvalid: true,\n          errorMsg:\n            uiSchema[cur]?.['ui:options']?.empty ||\n            `${properties[cur]?.title} ${t('empty')}`,\n        };\n        return acc;\n      }, formData || {});\n      if (onChange instanceof Function) {\n        onChange({ ...formData });\n      }\n      scrollElementIntoView(document.getElementById(errors[0]));\n      return false;\n    }\n    const syncErrors = await syncValidator();\n    if (syncErrors.length > 0) {\n      formData = syncErrors.reduce((acc, cur) => {\n        acc[cur.key] = {\n          ...formData![cur.key],\n          isInvalid: true,\n          errorMsg: cur.msg || `${properties[cur.key].title} ${t('invalid')}`,\n        };\n        return acc;\n      }, formData || {});\n      if (onChange instanceof Function) {\n        onChange({ ...formData });\n      }\n      scrollElementIntoView(document.getElementById(syncErrors[0].key));\n      return false;\n    }\n    return true;\n  };\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    const isValid = await validator();\n    if (!isValid) {\n      return;\n    }\n\n    Object.keys(formData!).forEach((key) => {\n      formData![key].isInvalid = false;\n      formData![key].errorMsg = '';\n    });\n    if (onChange instanceof Function) {\n      onChange(formData!);\n    }\n    if (onSubmit instanceof Function) {\n      onSubmit(e);\n    }\n  };\n\n  useImperativeHandle(ref, () => ({\n    validator,\n  }));\n  if (!formData || !schema || isEmpty(schema.properties)) {\n    return null;\n  }\n\n  return (\n    <Form noValidate onSubmit={handleSubmit}>\n      {keys.map((key) => {\n        const {\n          title,\n          description,\n          enum: enumValues = [],\n          enumNames = [],\n          max_length = 0,\n          max,\n          min = 0,\n        } = properties[key];\n        const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =\n          uiSchema?.[key] || {};\n        formData ||= {};\n        const fieldState = formData[key];\n        if (uiOpt?.class_name) {\n          uiOpt.className = uiOpt.class_name;\n        }\n\n        const uiSimplify = widget === 'legend' || uiOpt?.simplify;\n        let groupClassName: BaseUIOptions['field_class_name'] = uiOpt?.simplify\n          ? 'mb-2'\n          : 'mb-3';\n        if (widget === 'legend') {\n          groupClassName = 'mb-0';\n        }\n        if (uiOpt?.field_class_name) {\n          groupClassName = uiOpt.field_class_name;\n        }\n\n        const readOnly = uiOpt?.readOnly || false;\n\n        return (\n          <Form.Group\n            key={`${title}-${key}`}\n            controlId={key}\n            className={classnames(\n              groupClassName,\n              formData[key]?.hidden ? 'd-none' : null,\n            )}>\n            {/* Uniform processing `label` */}\n            {title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}\n            {/* Handling of individual specific controls */}\n            {widget === 'legend' ? (\n              <Legend title={title} className={String(uiOpt?.className)} />\n            ) : null}\n            {widget === 'select' ? (\n              <Select\n                desc={description}\n                fieldName={key}\n                onChange={onChange}\n                enumValues={enumValues}\n                enumNames={enumNames}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'radio' || widget === 'checkbox' ? (\n              <Check\n                type={widget}\n                fieldName={key}\n                onChange={onChange}\n                enumValues={enumValues}\n                enumNames={enumNames}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'switch' ? (\n              <Switch\n                label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'timezone' ? (\n              <Timezone\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'upload' ? (\n              <Upload\n                type={\n                  uiOpt && 'imageType' in uiOpt ? uiOpt.imageType : undefined\n                }\n                acceptType={\n                  uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''\n                }\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                readOnly={readOnly}\n                imgClassNames={\n                  uiOpt && 'className' in uiOpt ? uiOpt.className : ''\n                }\n              />\n            ) : null}\n            {widget === 'textarea' ? (\n              <Textarea\n                placeholder={\n                  uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''\n                }\n                rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}\n                className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'input' ? (\n              <Input\n                type={uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'}\n                inputMode={\n                  uiOpt && 'inputMode' in uiOpt ? uiOpt.inputMode : 'text'\n                }\n                placeholder={\n                  uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''\n                }\n                min={min}\n                max={max}\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                readOnly={readOnly}\n              />\n            ) : null}\n            {widget === 'button' ? (\n              <SfButton\n                fieldName={key}\n                text={uiOpt && 'text' in uiOpt ? uiOpt.text : ''}\n                action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}\n                formKit={formKitWithContext}\n                readOnly={readOnly}\n                variant={\n                  uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined\n                }\n                size={uiOpt && 'size' in uiOpt ? uiOpt.size : undefined}\n                title={uiOpt && 'title' in uiOpt ? uiOpt?.title : ''}\n              />\n            ) : null}\n            {widget === 'input_group' ? (\n              <InputGroup\n                formKitWithContext={formKitWithContext}\n                uiOpt={uiOpt as InputGroupOptions}\n                prefixText={\n                  (uiOpt && 'prefixText' in uiOpt && uiOpt.prefixText) || ''\n                }\n                suffixText={\n                  (uiOpt && 'suffixText' in uiOpt && uiOpt.suffixText) || ''\n                }>\n                <Input\n                  type={\n                    uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'\n                  }\n                  inputMode={\n                    uiOpt && 'inputMode' in uiOpt ? uiOpt.inputMode : 'text'\n                  }\n                  placeholder={\n                    uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''\n                  }\n                  min={min}\n                  max={max}\n                  fieldName={key}\n                  onChange={onChange}\n                  formData={formData}\n                  readOnly={readOnly}\n                />\n              </InputGroup>\n            ) : null}\n            {widget === 'tag_selector' ? (\n              <TagSelector\n                maxTagLength={max_length}\n                fieldName={key}\n                onChange={onChange}\n                formData={formData}\n                description={description}\n              />\n            ) : null}\n            {/* Unified handling of `Feedback` and `Text` */}\n            {description && widget !== 'tag_selector' ? (\n              <Form.Text dangerouslySetInnerHTML={{ __html: description }} />\n            ) : null}\n            <Form.Control.Feedback type=\"invalid\">\n              {fieldState?.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n        );\n      })}\n      {!hiddenSubmit && (\n        <Button variant=\"primary\" type=\"submit\">\n          {t('btn_submit')}\n        </Button>\n      )}\n    </Form>\n  );\n};\n\nexport const initFormData = (schema: JSONSchema): Type.FormDataType => {\n  const formData: Type.FormDataType = {};\n  const props: JSONSchema['properties'] = schema?.properties || {};\n  Object.keys(props).forEach((key) => {\n    const prop = props[key];\n    let defaultVal: any = '';\n    if (Array.isArray(prop.default) && prop.enum && prop.enum.length > 0) {\n      // for checkbox default values\n      defaultVal = prop.enum;\n    } else {\n      defaultVal = prop?.default;\n    }\n    formData[key] = {\n      value: defaultVal,\n      isInvalid: false,\n      errorMsg: '',\n    };\n  });\n  return formData;\n};\n\nexport const mergeFormData = (\n  target: Type.FormDataType | null,\n  origin: Type.FormDataType | null,\n) => {\n  if (!target) {\n    return origin;\n  }\n  if (!origin) {\n    return target;\n  }\n  Object.keys(target).forEach((k) => {\n    const oi = origin[k];\n    if (oi && oi.value !== undefined) {\n      target[k] = {\n        value: oi.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n  });\n  return target;\n};\n\nexport default forwardRef(SchemaForm);\n"
  },
  {
    "path": "ui/src/components/SchemaForm/types.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { ButtonProps } from 'react-bootstrap';\nimport React from 'react';\n\nimport classnames from 'classnames';\n\nimport * as Type from '@/common/interface';\n\nexport interface FormProps {\n  schema: JSONSchema | null;\n  uiSchema?: UISchema;\n  formData: Type.FormDataType | null;\n  refreshConfig?: FormKit['refreshConfig'];\n  hiddenSubmit?: boolean;\n  onChange?: (data: Type.FormDataType) => void;\n  onSubmit?: (e: React.FormEvent) => void;\n}\n\nexport interface FormRef {\n  validator: () => Promise<boolean>;\n}\n\nexport interface JSONSchema {\n  title: string;\n  description?: string;\n  required?: string[];\n  properties: {\n    [key: string]: {\n      type?: 'string' | 'boolean' | 'number' | Type.Tag[];\n      title: string;\n      description?: string;\n      enum?: Array<string | boolean | number>;\n      enumNames?: string[];\n      min?: number;\n      max?: number;\n      default?: string | boolean | number | any[];\n      max_length?: number;\n    };\n  };\n}\n\nexport interface BaseUIOptions {\n  empty?: string;\n  // Will be appended to the className of the form component itself\n  className?: classnames.Argument;\n  class_name?: classnames.Argument;\n  // The className that will be attached to a **form field container**\n  field_class_name?: classnames.Argument;\n  // Make a form component render into simplified mode\n  readOnly?: boolean;\n  simplify?: boolean;\n  validator?: (\n    value,\n    formData?,\n  ) => Promise<string | true | void> | true | string;\n}\n\nexport interface InputOptions extends BaseUIOptions {\n  placeholder?: string;\n  inputType?:\n    | 'color'\n    | 'date'\n    | 'datetime-local'\n    | 'email'\n    | 'month'\n    | 'number'\n    | 'password'\n    | 'range'\n    | 'search'\n    | 'tel'\n    | 'text'\n    | 'time'\n    | 'url'\n    | 'week';\n  inputMode?:\n    | 'text'\n    | 'search'\n    | 'none'\n    | 'tel'\n    | 'url'\n    | 'email'\n    | 'numeric'\n    | 'decimal'\n    | undefined;\n}\nexport interface SelectOptions extends BaseUIOptions {}\nexport interface UploadOptions extends BaseUIOptions {\n  acceptType?: string;\n  imageType?: Type.UploadType;\n}\n\nexport interface SwitchOptions extends BaseUIOptions {\n  label?: string;\n}\n\nexport interface TimezoneOptions extends BaseUIOptions {\n  placeholder?: string;\n}\n\nexport interface CheckboxOptions extends BaseUIOptions {}\n\nexport interface RadioOptions extends BaseUIOptions {}\n\nexport interface TextareaOptions extends BaseUIOptions {\n  placeholder?: string;\n  rows?: number;\n}\n\nexport interface ButtonOptions extends BaseUIOptions {\n  text: string;\n  iconName?: string;\n  action?: UIAction;\n  actionType: 'click' | 'submit';\n  variant?: ButtonProps['variant'];\n  size?: ButtonProps['size'];\n  title?: string;\n  clickCallback?: () => void;\n}\n\nexport interface InputGroupOptions extends InputOptions {\n  prefixText?: string;\n  suffixText?: string;\n  prefixBtnOptions?: ButtonOptions;\n  suffixBtnOptions?: ButtonOptions;\n}\n\nexport type UIOptions =\n  | InputOptions\n  | SelectOptions\n  | UploadOptions\n  | SwitchOptions\n  | TimezoneOptions\n  | CheckboxOptions\n  | RadioOptions\n  | TextareaOptions\n  | ButtonOptions\n  | InputGroupOptions;\n\nexport type UIWidget =\n  | 'textarea'\n  | 'input'\n  | 'checkbox'\n  | 'radio'\n  | 'select'\n  | 'upload'\n  | 'timezone'\n  | 'switch'\n  | 'legend'\n  | 'button'\n  | 'input_group'\n  | 'tag_selector';\nexport interface UISchema {\n  [key: string]: {\n    'ui:widget'?: UIWidget;\n    'ui:options'?: UIOptions;\n  };\n}\n\n/**\n * A few notes on button control：\n *  - Mainly used to send a request and notify the result of the request, and to update the data as required\n *  - A scenario where a message notification is displayed directly after a click without sending a request, implementing a dedicated control\n *  - Scenarios where the page jumps directly after a click without sending a request, implementing a dedicated control\n *\n * @field url : Target address for sending requests\n * @field method : Method for sending requests, default `get`\n * @field callback: Button event handler function that will fully take over the button events when this field is configured\n *                 *** Incomplete, DO NOT USE ***\n * @field loading: Set button loading information\n * @field on_complete: What needs to be done when the `Action` completes\n * @field on_complete.toast_return_message: Does toast show the returned message\n * @field on_complete.refresh_form_config: Whether to refresh the form configuration (configuration only, no data included)\n */\nexport interface UIAction {\n  url: string;\n  method?: 'get' | 'post' | 'put' | 'delete';\n  loading?: {\n    text: string;\n    state?: 'none' | 'pending' | 'completed';\n  };\n  on_complete?: {\n    toast_return_message?: boolean;\n    refresh_form_config?: boolean;\n  };\n}\n\n/**\n * Form tools\n * - Used to get or set the configuration of forms and form items, the value of a form item\n * * @method refreshConfig(): void\n */\n\nexport interface FormKit {\n  refreshConfig(): void;\n}\n"
  },
  {
    "path": "ui/src/components/Sender/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.sender-wrap {\n  z-index: 10;\n  margin-top: auto;\n  background-color: var(--bs-body-bg);\n  .input {\n    resize: none;\n    overflow-y: auto;\n    scrollbar-width: thin;\n  }\n  .input:focus {\n    box-shadow: none !important;\n    border-width: 0 !important;\n  }\n\n  .form-control-focus {\n    box-shadow: 0 0 0 0.25rem rgba(0, 51, 255, 0.25) !important;\n    border-color: rgb(128, 153, 255) !important;\n  }\n}\n"
  },
  {
    "path": "ui/src/components/Sender/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState, useRef, FC } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport { Icon } from '@/components';\n\nimport './index.scss';\n\ninterface IProps {\n  onSubmit?: (value: string) => void;\n  onCancel?: () => void;\n  isGenerate: boolean;\n  hasConversation: boolean;\n}\n\nconst Sender: FC<IProps> = ({\n  onSubmit,\n  onCancel,\n  isGenerate,\n  hasConversation,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });\n  const containerRef = useRef<HTMLDivElement>(null);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const [initialized, setInitialized] = useState(false);\n  const [inputValue, setInputValue] = useState('');\n  const [isFocus, setIsFocus] = useState(false);\n\n  const handleFocus = () => {\n    setIsFocus(true);\n    textareaRef?.current?.focus();\n  };\n\n  const handleBlur = () => {\n    setIsFocus(false);\n  };\n\n  const autoResize = () => {\n    const textarea = textareaRef.current;\n    if (!textarea) return;\n\n    textarea.style.height = '32px';\n\n    const minHeight = 32; // minimum height\n    const maxHeight = 96; // maximum height\n\n    // calculate the height needed\n    const { scrollHeight } = textarea;\n    const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);\n\n    // set the new height\n    textarea.style.height = `${newHeight}px`;\n\n    // control the scrollbar display\n    if (scrollHeight > maxHeight) {\n      textarea.style.overflowY = 'auto';\n    } else {\n      textarea.style.overflowY = 'hidden';\n    }\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setInputValue(e.target.value);\n    setTimeout(autoResize, 0);\n  };\n\n  const handleSubmit = () => {\n    if (isGenerate || !inputValue.trim()) {\n      return;\n    }\n    onSubmit?.(inputValue);\n    setInputValue('');\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault(); // Prevent default behavior of Enter key\n      handleSubmit();\n    } else if (e.key === 'Escape') {\n      setInputValue((prev) => `${prev}\\n`); // Add a new line on Escape key\n    }\n  };\n\n  useEffect(() => {\n    setInitialized(true);\n  }, []);\n\n  useEffect(() => {\n    const handleOutsideClick = (event) => {\n      if (\n        initialized &&\n        containerRef.current &&\n        !containerRef.current?.contains(event.target)\n      ) {\n        handleBlur();\n      }\n    };\n    document.addEventListener('click', handleOutsideClick);\n    return () => {\n      document.removeEventListener('click', handleOutsideClick);\n    };\n  }, [initialized]);\n  return (\n    <div\n      className={classnames(\n        'sender-wrap',\n        hasConversation ? 'sticky-bottom pb-4' : 'mt-0',\n      )}\n      ref={containerRef}>\n      <div\n        onClick={handleFocus}\n        className={classnames(\n          'position-relative form-control p-3',\n          isFocus ? 'form-control-focus' : '',\n        )}>\n        <Form.Control\n          as=\"textarea\"\n          ref={textareaRef}\n          style={{ height: '32px' }}\n          className=\"input border-0 p-0\"\n          placeholder={t('ask_placeholder')}\n          value={inputValue}\n          onFocus={handleFocus}\n          onChange={handleInputChange}\n          onKeyDown={handleKeyDown}\n        />\n        <div className=\"clearfix tools\">\n          {isGenerate ? (\n            <Button\n              variant=\"link\"\n              onClick={onCancel}\n              className=\"p-0 lh-1 link-dark float-end\">\n              <Icon name=\"stop-circle-fill\" size=\"24px\" />\n            </Button>\n          ) : (\n            <Button\n              variant=\"link\"\n              className=\"p-0 lh-1 link-dark float-end\"\n              onClick={handleSubmit}>\n              <Icon name=\"arrow-up-circle-fill\" size=\"24px\" />\n            </Button>\n          )}\n        </div>\n      </div>\n\n      <Form.Text className=\"text-center d-block\">{t('ai_generate')}</Form.Text>\n    </div>\n  );\n};\n\nexport default Sender;\n"
  },
  {
    "path": "ui/src/components/Share/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState, useEffect } from 'react';\nimport { Dropdown, OverlayTrigger, Tooltip } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { FacebookShareButton, TwitterShareButton } from 'next-share';\nimport copy from 'copy-to-clipboard';\nimport classNames from 'classnames';\n\nimport { BASE_ORIGIN } from '@/router/alias';\nimport { loggedUserInfoStore } from '@/stores';\n\ninterface IProps {\n  type: 'answer' | 'question';\n  qid: any;\n  aid?: any;\n  title: string;\n  className?: string;\n  mode?: 'normal' | 'mobile';\n  // slugTitle: string;\n}\n\nconst Index: FC<IProps> = ({ type, qid, aid, title, className, mode }) => {\n  const user = loggedUserInfoStore((state) => state.user);\n  const [show, setShow] = useState(false);\n  const [showTip, setShowTip] = useState(false);\n  const [canSystemShare, setSystemShareState] = useState(false);\n  const { t } = useTranslation();\n  let baseUrl =\n    type === 'question'\n      ? `${BASE_ORIGIN}/questions/${qid}`\n      : `${BASE_ORIGIN}/questions/${qid}/${aid}`;\n  if (user.id) {\n    baseUrl = `${baseUrl}?share=${user.username}`;\n  }\n\n  const closeShare = () => {\n    setShowTip(false);\n    setShow(false);\n  };\n\n  const handleCopy = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    let copyText = baseUrl;\n    if (title) {\n      copyText = `${title} ${baseUrl}`;\n    }\n    copy(copyText);\n    setShowTip(true);\n    setTimeout(closeShare, 1000);\n  };\n\n  const systemShare = () => {\n    navigator.share({\n      title,\n      text: `${title} - Answer：`,\n      url: baseUrl,\n    });\n  };\n  useEffect(() => {\n    if (window.navigator?.canShare?.({ text: 'can_share' })) {\n      setSystemShareState(true);\n    }\n  }, []);\n\n  if (mode === 'mobile') {\n    if (canSystemShare) {\n      return (\n        <Dropdown.Item onClick={systemShare}>{t('share.name')}</Dropdown.Item>\n      );\n    }\n    return null;\n  }\n  return (\n    <Dropdown show={show} onToggle={closeShare}>\n      <Dropdown.Toggle\n        id=\"dropdown-share\"\n        as=\"a\"\n        className={classNames('no-toggle pointer d-flex', className)}\n        onClick={() => setShow(true)}\n        style={{ lineHeight: '23px' }}>\n        {t('share.name')}\n      </Dropdown.Toggle>\n      <Dropdown.Menu style={{ minWidth: '195px' }}>\n        <OverlayTrigger\n          trigger=\"click\"\n          placement=\"left\"\n          show={showTip}\n          overlay={<Tooltip>{t('share.copied')}</Tooltip>}>\n          <Dropdown.Item onClick={handleCopy} eventKey=\"copy\">\n            {t('share.copy')}\n          </Dropdown.Item>\n        </OverlayTrigger>\n        <Dropdown.Item eventKey=\"facebook\">\n          <FacebookShareButton\n            title={title}\n            url={baseUrl}\n            className=\"w-100 py-1 px-3 text-start\">\n            {t('share.facebook')}\n          </FacebookShareButton>\n        </Dropdown.Item>\n        <Dropdown.Item>\n          <TwitterShareButton\n            title={title}\n            url={baseUrl}\n            className=\"w-100 py-1 px-3 text-start\">\n            {t('share.twitter')}\n          </TwitterShareButton>\n        </Dropdown.Item>\n        {canSystemShare && (\n          <Dropdown.Item onClick={systemShare}>{t('share.via')}</Dropdown.Item>\n        )}\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/SideNav/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n#pcSideNav {\n  width: 240px;\n  top: 64px;\n  box-sizing: border-box;\n  height: calc(100vh - 64px);\n  overflow-y: auto;\n  flex: none;\n}\n\n#sideNav {\n  max-width: 208px;\n  .nav-link {\n    color: var(--an-side-nav-link);\n  }\n  .nav-link:focus-visible {\n    box-shadow: none;\n  }\n  .nav-link:hover {\n    color: var(--an-side-nav-link-hover-color);\n    background-color: var(--bs-gray-100);\n  }\n  .nav-link.active {\n    color: var(--an-side-nav-link-hover-color);\n    background-color: var(--bs-gray-200);\n  }\n}\n\n@media screen and (max-width: 991.9px) {\n  .side-left-nav-wrap {\n    .nav {\n      max-width: 100%;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/components/SideNav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Nav } from 'react-bootstrap';\nimport { NavLink, useLocation, useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { loggedUserInfoStore, sideNavStore, aiControlStore } from '@/stores';\nimport { Icon, PluginRender } from '@/components';\nimport { PluginType } from '@/utils/pluginKit';\nimport request from '@/utils/request';\n\nimport './index.scss';\n\nconst Index: FC = () => {\n  const { t } = useTranslation();\n  const { pathname } = useLocation();\n  const { user: userInfo } = loggedUserInfoStore();\n  const { can_revision, revision } = sideNavStore();\n  const { ai_enabled } = aiControlStore();\n  const navigate = useNavigate();\n\n  return (\n    <Nav variant=\"pills\" className=\"flex-column\" id=\"sideNav\">\n      <NavLink\n        to=\"/questions\"\n        className={({ isActive }) =>\n          isActive || pathname === '/' ? 'nav-link active' : 'nav-link'\n        }>\n        <Icon name=\"question-circle-fill\" className=\"me-2\" />\n        <span>{t('header.nav.question')}</span>\n      </NavLink>\n\n      {ai_enabled && (\n        <NavLink\n          to=\"/ai-assistant\"\n          className={() =>\n            pathname === '/ai-assistant' ? 'nav-link active' : 'nav-link'\n          }>\n          <Icon name=\"chat-square-text-fill\" className=\"me-2\" />\n          <span>{t('ai_assistant', { keyPrefix: 'page_title' })}</span>\n        </NavLink>\n      )}\n\n      <NavLink\n        to=\"/tags\"\n        className={() =>\n          pathname === '/tags' ? 'nav-link active' : 'nav-link'\n        }>\n        <Icon name=\"tags-fill\" className=\"me-2\" />\n        <span>{t('header.nav.tag')}</span>\n      </NavLink>\n\n      <NavLink to=\"/users\" className=\"nav-link\">\n        <Icon name=\"people-fill\" className=\"me-2\" />\n        <span>{t('header.nav.user')}</span>\n      </NavLink>\n\n      <NavLink to=\"/badges\" className=\"nav-link\">\n        <Icon name=\"award-fill\" className=\"me-2\" />\n        <span>{t('header.nav.badges')}</span>\n      </NavLink>\n\n      <PluginRender\n        slug_name=\"quick_links\"\n        type={PluginType.Sidebar}\n        request={request}\n        navigate={navigate}\n      />\n\n      {can_revision || userInfo?.role_id === 2 ? (\n        <>\n          <div className=\"py-2 px-3 mt-3 small fw-bold\">\n            {t('header.nav.moderation')}\n          </div>\n          {can_revision && (\n            <NavLink to=\"/review\" className=\"nav-link\">\n              <Icon name=\"shield-fill-check\" className=\"me-2\" />\n              <span>{t('header.nav.review')}</span>\n              <span className=\"float-end\">\n                {revision > 99 ? '99+' : revision > 0 ? revision : ''}\n              </span>\n            </NavLink>\n          )}\n\n          {userInfo?.role_id === 2 ? (\n            <NavLink to=\"/admin\" className=\"nav-link\">\n              <Icon name=\"gear-fill\" className=\"me-2\" />\n              <span>{t('header.nav.admin')}</span>\n            </NavLink>\n          ) : null}\n        </>\n      ) : null}\n    </Nav>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/components/TabNav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Nav } from 'react-bootstrap';\nimport { NavLink, useLocation } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nconst TabNav: FC<{ menus: { name: string; path: string }[] }> = ({ menus }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });\n  const { pathname } = useLocation();\n  return (\n    <Nav variant=\"underline\" className=\"mb-4 border-bottom\">\n      {menus.map((menu) => (\n        <Nav.Item key={menu.path}>\n          <NavLink\n            to={menu.path}\n            className={() =>\n              pathname === menu.path ? 'nav-link active' : 'nav-link'\n            }>\n            {t(menu.name)}\n          </NavLink>\n        </Nav.Item>\n      ))}\n    </Nav>\n  );\n};\n\nexport default TabNav;\n"
  },
  {
    "path": "ui/src/components/Tag/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { memo, FC } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { Tag } from '@/common/interface';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface IProps {\n  data: Tag;\n  href?: string;\n  className?: string;\n  textClassName?: string;\n}\n\nconst Index: FC<IProps> = ({\n  data,\n  href,\n  className = '',\n  textClassName = '',\n}) => {\n  href ||= pathFactory.tagLanding(data.slug_name);\n\n  return (\n    <Link\n      to={href}\n      onClick={(e) => {\n        e.stopPropagation();\n      }}\n      className={classNames(\n        'badge-tag rounded-1',\n        data.reserved && 'badge-tag-reserved',\n        data.recommend && 'badge-tag-required',\n        className,\n      )}>\n      <span className={textClassName}>\n        {data.display_name || data.slug_name}\n      </span>\n    </Link>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/TagSelector/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.tag-selector-wrap {\n  .dropdown-menu {\n    min-width: 15rem;\n    max-height: 178px;\n    overflow-y: auto;\n  }\n  .dropdown-toggle {\n    &::after {\n      display: none;\n    }\n  }\n\n  .dropdown-item.active {\n    color: var(--bs-body-color);\n    background-color: var(--an-invite-answer-item-active);\n  }\n  .hover-hand:hover {\n    cursor: pointer;\n  }\n\n  .a-placeholder {\n    color: var(--bs-secondary-color);\n  }\n\n  .a-input {\n    padding: 0;\n    background: transparent !important;\n    box-shadow: none;\n    border: none;\n    min-width: 60px;\n  }\n  .a-input:focus {\n    box-shadow: none !important;\n    border: none;\n  }\n\n  .a-input-width {\n    display: inline-block;\n    position: absolute;\n    height: 0;\n    overflow: hidden;\n    max-width: calc(100% - 40px);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .a-placeholder-width {\n    display: inline-block;\n    position: absolute;\n    height: 0;\n    overflow: hidden;\n    max-width: calc(100% - 40px);\n  }\n}\n\n.tag-selector-wrap--focus {\n  color: var(--bs-body-color);\n  background-color: var(--bs-body-bg);\n  outline: 0;\n}\n"
  },
  {
    "path": "ui/src/components/TagSelector/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n/* eslint-disable no-nested-ternary */\nimport { FC, useState, useEffect, useRef, useCallback } from 'react';\nimport { Dropdown, Button, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport debounce from 'lodash/debounce';\nimport { marked } from 'marked';\nimport classNames from 'classnames';\n\nimport { useTagModal, useToast } from '@/hooks';\nimport type * as Type from '@/common/interface';\nimport { queryTags, useUserPermission } from '@/services';\nimport { writeSettingStore } from '@/stores';\n\n// import { OutsideClickListener } from '@/components';\n\nimport './index.scss';\n\ninterface IProps {\n  value?: Type.Tag[];\n  onChange?: (tags: Type.Tag[]) => void;\n  hiddenDescription?: boolean;\n  hiddenCreateBtn?: boolean;\n  maxTagLength?: number;\n  showRequiredTag?: boolean;\n  autoFocus?: boolean;\n  isInvalid?: boolean;\n  tagStyleMode?: 'default' | 'simple';\n  formText?: string;\n  errMsg?: string;\n}\n\nlet timer;\n\nconst TagSelector: FC<IProps> = ({\n  value = [],\n  onChange,\n  hiddenDescription = false,\n  hiddenCreateBtn = false,\n  maxTagLength = 0,\n  showRequiredTag = false,\n  autoFocus = false,\n  isInvalid = false,\n  formText = '',\n  tagStyleMode = 'default',\n  errMsg = '',\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [initialized, setInitialized] = useState(false);\n  const [focusState, setFocusState] = useState(autoFocus);\n  const [showMenu, setShowMenu] = useState(false);\n  const [currentIndex, setCurrentIndex] = useState<number>(0);\n  const [repeatIndex, setRepeatIndex] = useState(-1);\n  const [searchValue, setSearchValue] = useState<string>('');\n  const [tags, setTags] = useState<Type.Tag[] | null>([]);\n  const [requiredTags, setRequiredTags] = useState<Type.Tag[] | null>(null);\n  const writeInfo = writeSettingStore((state) => state.write);\n  const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });\n  const { data: userPermission } = useUserPermission('tag.add');\n  const canAddTag =\n    (maxTagLength > 0 && value?.length < maxTagLength) || maxTagLength === 0;\n  const toast = useToast();\n  const tagModal = useTagModal({\n    onConfirm: (data) => {\n      if (!(onChange instanceof Function)) {\n        return;\n      }\n      const findIndex = value.findIndex(\n        (item) => item.slug_name.toLowerCase() === data.slug_name.toLowerCase(),\n      );\n      if (findIndex === -1) {\n        onChange([\n          ...value,\n          {\n            ...data,\n            parsed_text: marked(data.original_text),\n          },\n        ]);\n        setSearchValue('');\n      } else {\n        setRepeatIndex(findIndex);\n        clearTimeout(timer);\n        timer = setTimeout(() => {\n          setRepeatIndex(-1);\n        }, 2000);\n      }\n    },\n  });\n\n  const filterTags = (result) => {\n    const tagArray: Type.Tag[] = [];\n    result?.forEach((item) => {\n      const findIndex = value.findIndex((v) => {\n        const tagName1 = v.slug_name.toLowerCase();\n        const tagName2 =\n          typeof item === 'string'\n            ? item.toLowerCase()\n            : item.slug_name.toLowerCase();\n\n        return tagName1 === tagName2;\n      });\n\n      if (findIndex === -1) {\n        tagArray.push(typeof item === 'string' ? { slug_name: item } : item);\n      }\n    });\n    return tagArray;\n  };\n\n  const handleMenuShow = (bol: boolean) => {\n    setShowMenu(bol);\n    const ele = document.getElementById('a-dropdown-menu');\n    if (ele) {\n      if (bol) {\n        ele.classList.add('show');\n      } else {\n        ele.classList.remove('show');\n      }\n    }\n  };\n\n  const handleTagSelectorFocus = () => {\n    setFocusState(true);\n    inputRef.current?.focus();\n  };\n\n  const handleTagSelectorBlur = () => {\n    setFocusState(false);\n    setCurrentIndex(0);\n    handleMenuShow(false);\n  };\n\n  const fetchTags = useCallback(\n    debounce((str) => {\n      if (!showRequiredTag && !str) {\n        setTags([]);\n        return;\n      }\n      queryTags(str).then((res) => {\n        const tagArray: Type.Tag[] = filterTags(res || []);\n        if (str === '') {\n          setRequiredTags(res);\n        }\n        handleMenuShow(tagArray.length > 0);\n        setTags(tagArray);\n      });\n    }, 400),\n    [],\n  );\n\n  const resetSearch = () => {\n    setCurrentIndex(0);\n    setSearchValue('');\n    if (canAddTag) {\n      const tagArray: Type.Tag[] = filterTags(requiredTags);\n      setTags(tagArray.length > 0 ? tagArray : []);\n    } else {\n      setTags([]);\n    }\n  };\n  const handleClick = (val: Type.Tag) => {\n    const findIndex = value.findIndex(\n      (item) => item.slug_name.toLowerCase() === val.slug_name.toLowerCase(),\n    );\n    if (onChange instanceof Function && findIndex === -1) {\n      onChange([\n        ...value,\n        {\n          original_text: '',\n          parsed_text: '',\n          ...val,\n        },\n      ]);\n    } else {\n      setRepeatIndex(findIndex);\n      clearTimeout(timer);\n      timer = setTimeout(() => {\n        setRepeatIndex(-1);\n      }, 2000);\n    }\n    resetSearch();\n  };\n\n  const handleRemove = (val: Type.Tag) => {\n    if (onChange instanceof Function) {\n      onChange(\n        value.filter((v) => {\n          if (v instanceof Object) {\n            return v.slug_name.toLowerCase() !== val.slug_name.toLowerCase();\n          }\n          return v !== val;\n        }),\n      );\n    }\n  };\n\n  const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const searchStr = e.currentTarget.value.replace(';', '');\n    onChange?.([...value]);\n    setSearchValue(searchStr);\n    fetchTags(searchStr);\n  };\n\n  const scrollIntoView = (targetId) => {\n    const container = document.getElementById('a-dropdown-menu') as HTMLElement;\n    const ele = document.getElementById(targetId) as HTMLElement;\n    if (ele?.offsetTop >= 104) {\n      container.scrollTo({\n        top: ele.offsetTop - 104,\n        behavior: 'smooth',\n      });\n    }\n  };\n\n  const handleKeyDown = (e) => {\n    e.stopPropagation();\n    const { keyCode } = e;\n    if (keyCode === 9) {\n      // handleTagSelectorBlur();\n      return;\n    }\n    if (value.length > 0 && keyCode === 8 && !searchValue) {\n      handleRemove(value[value.length - 1]);\n    }\n\n    if (!tags) {\n      return;\n    }\n\n    if (keyCode === 38 && currentIndex > 0) {\n      scrollIntoView(tags[currentIndex - 1].slug_name);\n      setCurrentIndex(currentIndex - 1);\n    }\n    if (keyCode === 40 && currentIndex < tags.length - 1) {\n      scrollIntoView(tags[currentIndex + 1].slug_name);\n      setCurrentIndex(currentIndex + 1);\n    }\n\n    if (keyCode === 13 && currentIndex > -1) {\n      e.preventDefault();\n      if (tags.length === 0 && searchValue) {\n        tagModal.onShow(searchValue);\n        return;\n      }\n      if (currentIndex <= tags.length - 1) {\n        handleClick(tags[currentIndex]);\n        // if (currentIndex === tags.length - 1 && currentIndex > 0) {\n        //   setCurrentIndex(currentIndex - 1);\n        // }\n      }\n    }\n  };\n\n  const handleCreate = () => {\n    const tagAddPermission = userPermission?.['tag.add'];\n    if (!tagAddPermission || tagAddPermission?.has_permission) {\n      tagModal.onShow(searchValue);\n    } else if (tagAddPermission?.no_permission_tip) {\n      toast.onShow({\n        msg: tagAddPermission.no_permission_tip,\n        variant: 'danger',\n      });\n    }\n  };\n\n  const handleClickToggle = () => {\n    const menuHasContent =\n      (tags && tags?.length > 0) ||\n      (searchValue && tags?.length === 0) ||\n      (searchValue && !hiddenCreateBtn);\n    if (canAddTag && menuHasContent) {\n      handleMenuShow(true);\n    } else {\n      handleMenuShow(false);\n    }\n  };\n\n  const handleTagHint = () => {\n    if (!writeInfo || writeInfo.min_tags === undefined || !writeInfo.min_tags) {\n      return t('hint_zero_tags');\n    }\n\n    if (writeInfo.min_tags === 1) {\n      return t('hint');\n    }\n\n    return t(`hint_more_than_one_tag`, {\n      min_tags_number: writeInfo.min_tags,\n    });\n  };\n\n  useEffect(() => {\n    if (canAddTag) {\n      const tagArray: Type.Tag[] = filterTags(requiredTags);\n      setTags(tagArray.length > 0 ? tagArray : []);\n    } else {\n      setTags([]);\n    }\n  }, [value]);\n\n  useEffect(() => {\n    if (focusState && showRequiredTag) {\n      fetchTags(searchValue);\n      inputRef.current?.focus();\n    }\n  }, [focusState]);\n\n  useEffect(() => {\n    setInitialized(true);\n  }, []);\n\n  useEffect(() => {\n    const handleOutsideClick = (event) => {\n      if (\n        initialized &&\n        containerRef.current &&\n        !containerRef.current?.contains(event.target)\n      ) {\n        handleTagSelectorBlur();\n      }\n    };\n    document.addEventListener('click', handleOutsideClick);\n    return () => {\n      document.removeEventListener('click', handleOutsideClick);\n    };\n  }, [initialized]);\n\n  useEffect(() => {\n    // menu show\n    const menuHasContent =\n      (tags && tags?.length > 0) ||\n      (searchValue && tags?.length === 0) ||\n      (searchValue && !hiddenCreateBtn);\n    if (focusState) {\n      if (canAddTag && menuHasContent) {\n        handleMenuShow(true);\n      } else {\n        handleMenuShow(false);\n      }\n\n      if ((tags && tags?.length < 5) || maxTagLength === 0) {\n        inputRef.current?.focus();\n      }\n    }\n  }, [focusState, tags, hiddenCreateBtn, searchValue, maxTagLength]);\n\n  useEffect(() => {\n    // set width of tag Form.Control\n    const ele = document.querySelector('.a-input-width') as HTMLElement;\n    const elePlaceholder = document.querySelector(\n      '.a-placeholder-width',\n    ) as HTMLElement;\n    if (ele.offsetWidth > 60) {\n      inputRef.current?.setAttribute(\n        'style',\n        `width:${ele.offsetWidth + 16}px`,\n      );\n    } else {\n      inputRef.current?.setAttribute(\n        'style',\n        `width: ${elePlaceholder.offsetWidth + 7}px`,\n      );\n    }\n  }, [searchValue]);\n\n  return (\n    <div ref={containerRef} className=\"position-relative\">\n      <div\n        className={classNames(\n          'tag-selector-wrap form-control position-relative p-0',\n          focusState ? 'tag-selector-wrap--focus' : '',\n          isInvalid ? 'is-invalid' : '',\n        )}\n        onClick={handleTagSelectorFocus}\n        onKeyDown={handleKeyDown}>\n        <div onClick={handleClickToggle}>\n          <div\n            className=\"d-flex flex-wrap m-n1\"\n            style={{ padding: '0.375rem 0.75rem' }}>\n            {value?.map((item, index) => {\n              return (\n                <span\n                  key={item.slug_name}\n                  className={classNames(\n                    'badge-tag rounded-1 m-1 flex-shrink-0',\n                    tagStyleMode === 'default' &&\n                      item.reserved &&\n                      'badge-tag-reserved',\n                    tagStyleMode === 'default' &&\n                      item.recommend &&\n                      'badge-tag-required',\n                    index === repeatIndex && 'bg-fade-out',\n                  )}>\n                  {item.display_name}\n                  <span\n                    className=\"ms-1 hover-hand\"\n                    onMouseUp={() => handleRemove(item)}>\n                    ×\n                  </span>\n                </span>\n              );\n            })}\n            {canAddTag ? (\n              <Form.Control\n                // autoFocus\n                autoComplete=\"off\"\n                style={{ width: '60px' }}\n                ref={inputRef}\n                className=\"a-input m-1\"\n                placeholder={t('add_btn')}\n                value={searchValue}\n                onChange={handleSearch}\n                onFocus={handleTagSelectorFocus}\n              />\n            ) : (\n              <Form.Control\n                autoComplete=\"off\"\n                className=\"a-input\"\n                style={{ width: '60px', position: 'absolute', zIndex: -1 }}\n                autoFocus\n              />\n            )}\n            <span className=\"a-input-width\">{searchValue}</span>\n            <span className=\"a-placeholder-width\">{t('add_btn')}</span>\n          </div>\n        </div>\n        <Dropdown.Menu id=\"a-dropdown-menu\" className=\"w-100\" show={showMenu}>\n          {!searchValue &&\n            showRequiredTag &&\n            tags &&\n            tags.filter((v) => v.recommend)?.length > 0 && (\n              <h6 className=\"dropdown-header\">{t('tag_required_text')}</h6>\n            )}\n\n          {tags?.map((item, index) => {\n            return (\n              <Dropdown.Item\n                key={item.slug_name}\n                id={item.slug_name}\n                active={index === currentIndex}\n                onClick={() => handleClick(item)}>\n                {item.display_name}\n              </Dropdown.Item>\n            );\n          })}\n          {searchValue && tags?.length === 0 && (\n            <Dropdown.Item disabled className=\"text-secondary\">\n              {t('no_result')}\n            </Dropdown.Item>\n          )}\n          {!hiddenCreateBtn && searchValue && (\n            <Button\n              variant=\"link\"\n              className=\"px-3 btn-no-border w-100 text-start\"\n              onClick={handleCreate}>\n              + {t('create_btn')}\n            </Button>\n          )}\n        </Dropdown.Menu>\n      </div>\n      {!hiddenDescription && (\n        <Form.Text>{formText || handleTagHint()}</Form.Text>\n      )}\n      <Form.Control.Feedback type=\"invalid\">{errMsg}</Form.Control.Feedback>\n    </div>\n  );\n};\n\nexport default TagSelector;\n"
  },
  {
    "path": "ui/src/components/TagsLoader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Col, Card } from 'react-bootstrap';\n\ninterface Props {\n  count?: number;\n}\n\nconst Index: FC<Props> = ({ count = 20 }) => {\n  const list = new Array(count).fill(0).map((v, i) => v + i);\n  return (\n    <>\n      {list.map((v) => (\n        <Col\n          key={v}\n          xs={12}\n          lg={3}\n          md={4}\n          sm={6}\n          className=\"mb-4 placeholder-glow\">\n          <Card className=\"h-100\">\n            <Card.Body className=\"d-flex flex-column align-items-start\">\n              <div\n                className=\"placeholder align-top w-25 mb-3\"\n                style={{ height: '24px' }}\n              />\n\n              <p\n                className=\"placeholder small text-truncate-3 w-100\"\n                style={{ height: '42px' }}\n              />\n              <div className=\"d-flex align-items-center\">\n                <div\n                  className=\"placeholder me-2\"\n                  style={{ width: '80px', height: '31px' }}\n                />\n                <span\n                  className=\"placeholder text-secondary small text-nowrap\"\n                  style={{ width: '100px', height: '21px' }}\n                />\n              </div>\n            </Card.Body>\n          </Card>\n        </Col>\n      ))}\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/TextArea/index.tsx",
    "content": "/*\r\n * Licensed to the Apache Software Foundation (ASF) under one\r\n * or more contributor license agreements.  See the NOTICE file\r\n * distributed with this work for additional information\r\n * regarding copyright ownership.  The ASF licenses this file\r\n * to you under the Apache License, Version 2.0 (the\r\n * \"License\"); you may not use this file except in compliance\r\n * with the License.  You may obtain a copy of the License at\r\n *\r\n *   http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing,\r\n * software distributed under the License is distributed on an\r\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n * KIND, either express or implied.  See the License for the\r\n * specific language governing permissions and limitations\r\n * under the License.\r\n */\r\n\r\nimport { FC, useRef, useEffect, memo } from 'react';\r\nimport { FormControl, FormControlProps } from 'react-bootstrap';\r\n\r\nconst TextArea: FC<\r\n  FormControlProps & { rows?: number; autoFocus?: boolean }\r\n> = ({\r\n  value,\r\n  onChange,\r\n  size,\r\n  rows = 1,\r\n  autoFocus = true,\r\n  isInvalid,\r\n  ...rest\r\n}) => {\r\n  const ref = useRef<HTMLTextAreaElement>(null);\r\n\r\n  const autoGrow = () => {\r\n    if (ref.current) {\r\n      ref.current.style.height = 'auto';\r\n      ref.current.style.height = `${ref.current.scrollHeight}px`;\r\n    }\r\n  };\r\n\r\n  useEffect(() => {\r\n    if (ref.current && value) {\r\n      autoGrow();\r\n    }\r\n  }, [ref, value]);\r\n\r\n  return (\r\n    <FormControl\r\n      as=\"textarea\"\r\n      className=\"resize-none font-monospace\"\r\n      rows={rows}\r\n      size={size}\r\n      value={value}\r\n      onChange={onChange}\r\n      autoFocus={autoFocus}\r\n      ref={ref}\r\n      onInput={autoGrow}\r\n      isInvalid={isInvalid}\r\n      {...rest}\r\n    />\r\n  );\r\n};\r\nexport default memo(TextArea);\r\n"
  },
  {
    "path": "ui/src/components/TimeZonePicker/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Form } from 'react-bootstrap';\n\nimport { TIMEZONES } from '@/common/constants';\n\nconst TimeZonePicker = (props) => {\n  return (\n    <Form.Select {...props}>\n      {TIMEZONES?.map((item) => {\n        return (\n          <optgroup label={item.label} key={item.label}>\n            {item.options.map((option) => {\n              return (\n                <option value={option.value} key={option.value}>\n                  {option.label}\n                </option>\n              );\n            })}\n          </optgroup>\n        );\n      })}\n    </Form.Select>\n  );\n};\n\nexport default TimeZonePicker;\n"
  },
  {
    "path": "ui/src/components/Toast/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Toast } from 'react-bootstrap';\n\ninterface IProps {\n  /** main content */\n  msg: string;\n  /** theme color */\n  variant?: 'warning' | 'success' | 'danger';\n  /** callback click close */\n  onClose: () => void;\n}\n\nconst Index: FC<IProps> = ({ msg, variant = 'warning', onClose }) => {\n  return (\n    <div\n      className=\"d-flex justify-content-center\"\n      style={{\n        position: 'fixed',\n        top: '78px',\n        left: 0,\n        right: 0,\n        margin: 'auto',\n        zIndex: 5,\n      }}>\n      <Toast\n        className=\"align-items-center border-0\"\n        delay={5000}\n        bg={variant}\n        show={Boolean(msg)}\n        autohide\n        onClose={onClose}>\n        <div className=\"d-flex\">\n          <Toast.Body\n            dangerouslySetInnerHTML={{ __html: msg }}\n            className={`${variant !== 'warning' ? 'text-white' : ''}`}\n          />\n          <button\n            className={`btn-close me-2 m-auto ${\n              variant !== 'warning' ? 'btn-close-white' : ''\n            }`}\n            onClick={onClose}\n            data-bs-dismiss=\"toast\"\n            aria-label=\"Close\"\n          />\n        </div>\n      </Toast>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/Unactivate/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState } from 'react';\nimport { Button, Col } from 'react-bootstrap';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport type { ImgCodeReq, FormDataType } from '@/common/interface';\nimport { loggedUserInfoStore } from '@/stores';\nimport { resendEmail } from '@/services';\nimport { handleFormError } from '@/utils';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\n\ninterface IProps {\n  visible?: boolean;\n}\n\nconst Index: React.FC<IProps> = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'inactive' });\n  const [isSuccess, setSuccess] = useState(false);\n  const { e_mail } = loggedUserInfoStore((state) => state.user);\n  const [formData, setFormData] = useState<FormDataType>({\n    captcha_code: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const emailCaptcha = useCaptchaPlugin('email');\n\n  const submit = () => {\n    let req: ImgCodeReq = {};\n    const imgCode = emailCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      req = {\n        captcha_code: imgCode.captcha_code,\n        captcha_id: imgCode.captcha_id,\n      };\n    }\n    resendEmail(req)\n      .then(async () => {\n        await emailCaptcha?.close();\n        setSuccess(true);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          emailCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n        }\n      });\n  };\n\n  const onSentEmail = (evt) => {\n    evt.preventDefault();\n    if (!emailCaptcha) {\n      submit();\n      return;\n    }\n    emailCaptcha.check(() => {\n      submit();\n    });\n  };\n\n  return (\n    <Col md={6} className=\"mx-auto text-center\">\n      {isSuccess ? (\n        <p>\n          <Trans\n            i18nKey=\"inactive.another\"\n            values={{ mail: e_mail }}\n            components={{ bold: <strong /> }}\n          />\n        </p>\n      ) : (\n        <>\n          <p>\n            <Trans\n              i18nKey=\"inactive.first\"\n              values={{ mail: e_mail }}\n              components={{ bold: <strong /> }}\n            />\n          </p>\n          <p>{t('info')}</p>\n          <Button variant=\"link\" onClick={onSentEmail}>\n            {t('btn_name')}\n          </Button>\n          <Link to=\"/users/change-email\" replace className=\"btn btn-link ms-2\">\n            {t('change_btn_name')}\n          </Link>\n        </>\n      )}\n    </Col>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/components/UploadImg/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport { uploadImage } from '@/services';\nimport * as Type from '@/common/interface';\n\ninterface IProps {\n  type: Type.UploadType;\n  className?: classnames.Argument;\n  children?: React.ReactNode;\n  acceptType?: string;\n  disabled?: boolean;\n  uploadCallback: (img: string) => void;\n}\n\nconst Index: React.FC<IProps> = ({\n  type,\n  uploadCallback,\n  children,\n  acceptType = '',\n  className,\n  disabled = false,\n}) => {\n  const { t } = useTranslation();\n  const [status, setStatus] = useState(false);\n\n  const onChange = (e: any) => {\n    if (status) {\n      return;\n    }\n    if (e.target.files[0]) {\n      // const fileSize = e.target.files[0].size || 0;\n\n      // if (maxSize && fileSize / 1024 / 1024 > 2) {\n      //   Modal.confirm({\n      //     content: '请上传小于 2M 的图片',\n      //   });\n      //   return;\n      // }\n      setStatus(true);\n      uploadImage({ file: e.target.files[0], type })\n        .then((res) => {\n          uploadCallback(res);\n        })\n        .finally(() => {\n          setStatus(false);\n        });\n    }\n  };\n\n  return (\n    <label\n      className={classnames('btn btn-outline-secondary', className, {\n        disabled: !!disabled,\n      })}>\n      {children || (status ? t('upload_img.loading') : t('upload_img.name'))}\n      <input\n        type=\"file\"\n        className=\"d-none\"\n        disabled={disabled}\n        accept={`image/jpeg,image/jpg,image/png,image/webp${acceptType}`}\n        onChange={onChange}\n      />\n    </label>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/components/UserCard/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Link } from 'react-router-dom';\n\nimport classnames from 'classnames';\n\nimport { Avatar, FormatTime } from '@/components';\nimport { formatCount } from '@/utils';\n\ninterface Props {\n  data: any;\n  time: number;\n  preFix?: string;\n  isLogged: boolean;\n  timelinePath: string;\n  className?: string;\n  updateTime?: number;\n  updateTimePrefix?: string;\n}\n\nconst Index: FC<Props> = ({\n  data,\n  time,\n  preFix,\n  isLogged,\n  timelinePath,\n  className = '',\n  updateTime = 0,\n  updateTimePrefix = '',\n}) => {\n  return (\n    <div className={classnames('d-flex', className)}>\n      {data?.status !== 'deleted' ? (\n        <Link to={`/users/${data?.username}`}>\n          <Avatar\n            avatar={data?.avatar}\n            size=\"40px\"\n            className=\"me-2 d-none d-md-block\"\n            searchStr=\"s=96\"\n            alt={data?.display_name}\n          />\n\n          <Avatar\n            avatar={data?.avatar}\n            size=\"24px\"\n            className=\"me-2 d-block d-md-none\"\n            searchStr=\"s=48\"\n            alt={data?.display_name}\n          />\n        </Link>\n      ) : (\n        <>\n          <Avatar\n            avatar={data?.avatar}\n            size=\"40px\"\n            className=\"me-2 d-none d-md-block\"\n            searchStr=\"s=96\"\n            alt={data?.display_name}\n          />\n\n          <Avatar\n            avatar={data?.avatar}\n            size=\"24px\"\n            className=\"me-2 d-block d-md-none\"\n            searchStr=\"s=48\"\n            alt={data?.display_name}\n          />\n        </>\n      )}\n      <div className=\"small text-secondary d-flex flex-column\">\n        <div className=\"me-1 me-md-0 d-flex align-items-center\">\n          {data?.status !== 'deleted' ? (\n            <Link\n              to={`/users/${data?.username}`}\n              className=\"me-1 text-break name-ellipsis\"\n              style={{ maxWidth: '100px' }}>\n              {data?.display_name}\n            </Link>\n          ) : (\n            <span className=\"me-1 text-break\">{data?.display_name}</span>\n          )}\n          <span className=\"fw-bold\" title=\"Reputation\">\n            {formatCount(data?.rank)}\n          </span>\n        </div>\n        {time &&\n          (isLogged ? (\n            <Link to={timelinePath}>\n              <FormatTime\n                time={time}\n                preFix={preFix}\n                className=\"link-secondary\"\n              />\n              {updateTime > 0 && (\n                <>\n                  <span className=\"mx-1 link-secondary\">•</span>\n                  <FormatTime\n                    time={updateTime}\n                    preFix={updateTimePrefix}\n                    className=\"link-secondary\"\n                  />\n                </>\n              )}\n            </Link>\n          ) : (\n            <>\n              <FormatTime time={time} preFix={preFix} />\n              {updateTime > 0 && (\n                <>\n                  <span className=\"mx-1 link-secondary\">•</span>\n                  <FormatTime\n                    time={updateTime}\n                    preFix={updateTimePrefix}\n                    className=\"link-secondary\"\n                  />\n                </>\n              )}\n            </>\n          ))}\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/WelcomeTitle/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport { siteInfoStore } from '@/stores';\n\ninterface Props {\n  as?: React.ElementType;\n  className?: string;\n}\n\nconst Index: FC<Props> = ({ as: Component = 'h3', className = 'mb-5' }) => {\n  const { t } = useTranslation();\n  const { name: siteName } = siteInfoStore((_) => _.siteInfo);\n  return (\n    <Component className={classnames('text-center', className)}>\n      {t('website_welcome', { site_name: siteName })}\n    </Component>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Avatar from './Avatar';\nimport Editor, { EditorRef, htmlRender } from './Editor';\nimport Header from './Header';\nimport Footer from './Footer';\nimport Icon from './Icon';\nimport SvgIcon from './Icon/svg';\nimport Modal from './Modal';\nimport TagSelector from './TagSelector';\nimport Unactivate from './Unactivate';\nimport UploadImg from './UploadImg';\nimport Actions from './Actions';\nimport Tag from './Tag';\nimport Operate from './Operate';\nimport UserCard from './UserCard';\nimport Pagination from './Pagination';\nimport Comment from './Comment';\nimport TextArea from './TextArea';\nimport Mentions from './Mentions';\nimport FormatTime from './FormatTime';\nimport Toast from './Toast';\nimport AccordionNav from './AccordionNav';\nimport Empty from './Empty';\nimport BaseUserCard from './BaseUserCard';\nimport FollowingTags from './FollowingTags';\nimport QueryGroup from './QueryGroup';\nimport BrandUpload from './BrandUpload';\nimport SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm';\nimport DiffContent from './DiffContent';\nimport Customize from './Customize';\nimport CustomizeTheme from './CustomizeTheme';\nimport PageTags from './PageTags';\nimport QuestionListLoader from './QuestionListLoader';\nimport TagsLoader from './TagsLoader';\nimport WelcomeTitle from './WelcomeTitle';\nimport Counts from './Counts';\nimport QuestionList from './QuestionList';\nimport HotQuestions from './HotQuestions';\nimport HttpErrorContent from './HttpErrorContent';\nimport CustomSidebar from './CustomSidebar';\nimport ImgViewer from './ImgViewer';\nimport SideNav from './SideNav';\nimport PluginRender from './PluginRender';\nimport HighlightText from './HighlightText';\nimport CardBadge from './CardBadge';\nimport PinList from './PinList';\nimport MobileSideNav from './MobileSideNav';\nimport AdminSideNav from './AdminSideNav';\nimport BubbleAi from './BubbleAi';\nimport BubbleUser from './BubbleUser';\nimport Sender from './Sender';\nimport TabNav from './TabNav';\n\nexport {\n  Avatar,\n  Header,\n  Footer,\n  Icon,\n  SvgIcon,\n  Modal,\n  Unactivate,\n  UploadImg,\n  Editor,\n  Tag,\n  TagSelector,\n  Actions,\n  Operate,\n  UserCard,\n  Pagination,\n  Comment,\n  TextArea,\n  Mentions,\n  FormatTime,\n  Toast,\n  AccordionNav,\n  Empty,\n  BaseUserCard,\n  FollowingTags,\n  htmlRender,\n  QueryGroup,\n  BrandUpload,\n  SchemaForm,\n  initFormData,\n  DiffContent,\n  Customize,\n  CustomizeTheme,\n  PageTags,\n  QuestionListLoader,\n  TagsLoader,\n  WelcomeTitle,\n  Counts,\n  QuestionList,\n  HotQuestions,\n  HttpErrorContent,\n  CustomSidebar,\n  ImgViewer,\n  SideNav,\n  PluginRender,\n  HighlightText,\n  CardBadge,\n  PinList,\n  MobileSideNav,\n  AdminSideNav,\n  BubbleAi,\n  BubbleUser,\n  Sender,\n  TabNav,\n};\nexport type { EditorRef, JSONSchema, UISchema };\n"
  },
  {
    "path": "ui/src/hooks/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useTagModal from './useTagModal';\nimport useToast from './useToast';\nimport useReportModal from './useReportModal';\nimport usePageUsers from './usePageUsers';\nimport useChangeUserRoleModal from './useChangeUserRoleModal';\nimport useUserModal from './useUserModal';\nimport useChangePasswordModal from './useChangePasswordModal';\nimport useChangeProfileModal from './useChangeProfileModal';\nimport usePageTags from './usePageTags';\nimport useLoginRedirect from './useLoginRedirect';\nimport usePromptWithUnload from './usePrompt';\nimport useActivationEmailModal from './useActivationEmailModal';\nimport useCaptchaModal from './useCaptchaModal';\nimport useSkeletonControl from './useSkeletonControl';\nimport useExternalToast from './useExternalToast';\n\nexport {\n  useTagModal,\n  useToast,\n  useReportModal,\n  usePageUsers,\n  useChangeUserRoleModal,\n  useUserModal,\n  useChangePasswordModal,\n  useChangeProfileModal,\n  usePageTags,\n  useLoginRedirect,\n  usePromptWithUnload,\n  useActivationEmailModal,\n  useCaptchaModal,\n  useSkeletonControl,\n  useExternalToast,\n};\n"
  },
  {
    "path": "ui/src/hooks/useActivationEmailModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState, useRef } from 'react';\nimport { Modal, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport type * as Type from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\nimport { handleFormError } from '@/utils';\nimport { getUserActivation, postUserActivation } from '@/services';\nimport { useToast } from '@/hooks';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface IProps {\n  title?: string;\n  onConfirm?: (formData: any) => Promise<any>;\n}\nconst useChangePasswordModal = (props: IProps = {}) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'inactive',\n  });\n\n  const { title = t('btn_name') } = props;\n  const [visible, setVisibleState] = useState(false);\n  const userId = useRef('');\n  const isLoading = useRef(false);\n  const Toast = useToast();\n\n  const schema: JSONSchema = {\n    title: t('btn_name'),\n    properties: {\n      activationUrl: {\n        type: 'string',\n        title: t('resend_email.url_label'),\n        description: t('resend_email.url_text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    activationUrl: {\n      'ui:options': {\n        readOnly: true,\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const formRef = useRef<{\n    validator: () => Promise<boolean>;\n  }>(null);\n\n  const getActivationUrl = () => {\n    return getUserActivation(userId.current).then((resp) => {\n      if (resp?.activation_url) {\n        setFormData({\n          ...formData,\n          activationUrl: {\n            value: resp.activation_url,\n            isInvalid: false,\n            errorMsg: '',\n          },\n        });\n      }\n    });\n  };\n\n  const onClose = () => {\n    setVisibleState(false);\n    userId.current = '';\n    setFormData(initFormData(schema));\n  };\n\n  const onShow = async (user_id: string) => {\n    if (!user_id) {\n      return;\n    }\n    userId.current = user_id;\n    await getActivationUrl();\n    setVisibleState(true);\n  };\n\n  const handleSubmit = async (event) => {\n    event.preventDefault();\n    event.stopPropagation();\n    isLoading.current = true;\n    postUserActivation(userId.current)\n      .then(() => {\n        Toast.onShow({\n          msg: t('sent_success', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        onClose();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n        }\n      })\n      .finally(() => {\n        isLoading.current = false;\n      });\n  };\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={visible} title={title} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <SchemaForm\n            ref={formRef}\n            schema={schema}\n            uiSchema={uiSchema}\n            formData={formData}\n            onChange={handleOnChange}\n            hiddenSubmit\n          />\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('cancel', { keyPrefix: 'btns' })}\n          </Button>\n          <Button\n            disabled={isLoading.current}\n            variant=\"primary\"\n            onClick={handleSubmit}>\n            {t('resend', { keyPrefix: 'btns' })}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useChangePasswordModal;\n"
  },
  {
    "path": "ui/src/hooks/useCaptchaModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useRef, useState, useLayoutEffect } from 'react';\nimport { Modal, Form, Button, InputGroup } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport { Icon } from '@/components';\nimport type {\n  FormValue,\n  ImgCodeRes,\n  CaptchaKey,\n  FieldError,\n  ImgCodeReq,\n} from '@/common/interface';\nimport { checkImgCode } from '@/services';\n\ntype SubmitCallback = {\n  (): void;\n};\n\nconst Index = (captchaKey: CaptchaKey) => {\n  const refRoot = useRef(null);\n  if (refRoot.current === null) {\n    // @ts-ignore\n    refRoot.current = ReactDOM.createRoot(document.createElement('div'));\n  }\n\n  const { t } = useTranslation('translation', { keyPrefix: 'pic_auth_code' });\n  const refKey = useRef<CaptchaKey>(captchaKey);\n  const refCallback = useRef<SubmitCallback>();\n  const pending = useRef(false);\n  const autoInitCaptchaData = /email/i.test(refKey.current);\n\n  const [stateShow, setStateShow] = useState(false);\n  const [captcha, setCaptcha] = useState<ImgCodeRes>({\n    captcha_id: '',\n    captcha_img: '',\n    verify: false,\n  });\n  const [imgCode, setImgCode] = useState<FormValue>({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  });\n  const refCaptcha = useRef(captcha);\n  const refImgCode = useRef(imgCode);\n\n  const fetchCaptchaData = () => {\n    pending.current = true;\n    checkImgCode(refKey.current)\n      .then((resp) => {\n        setCaptcha(resp);\n      })\n      .finally(() => {\n        pending.current = false;\n      });\n  };\n\n  const resetCapture = () => {\n    setCaptcha({\n      captcha_id: '',\n      captcha_img: '',\n      verify: false,\n    });\n  };\n\n  const resetImgCode = () => {\n    setImgCode({\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    });\n  };\n  const resetCallback = () => {\n    refCallback.current = undefined;\n  };\n\n  const show = () => {\n    if (!stateShow) {\n      setStateShow(true);\n    }\n  };\n  /**\n   * There are some cases where the React scheduler cancels the execution of some functions,\n   * which prevents them from closing properly:\n   *  for example, if the parent component uninstalls the child component directly,\n   *  and the `captchaModal.close()` call is inside the child component.\n   * In this case, call `await captchaModal.close()` and wait for the close action to complete.\n   */\n  const close = () => {\n    setStateShow(false);\n    resetCapture();\n    resetImgCode();\n    resetCallback();\n\n    const p = new Promise<void>((resolve) => {\n      setTimeout(resolve, 50);\n    });\n    return p;\n  };\n\n  const handleCaptchaError = (fel: FieldError[] = []) => {\n    const captchaErr = fel.find((o) => {\n      return o.error_field === 'captcha_code';\n    });\n\n    const ri = refImgCode.current;\n    if (captchaErr) {\n      /**\n       * `imgCode.value` No value but a validation error is received,\n       * indicating that it is the first time the interface has returned a CAPTCHA error,\n       * triggering the CAPTCHA logic. There is no need to display the error message at this point.\n       */\n      if (ri.value) {\n        setImgCode({\n          ...ri,\n          isInvalid: true,\n          errorMsg: captchaErr.error_msg,\n        });\n      }\n      fetchCaptchaData();\n      show();\n    } else {\n      close();\n    }\n    // Assist business logic in filtering CAPTCHA error messages when necessary\n    return captchaErr;\n  };\n\n  const handleChange = (evt) => {\n    evt.preventDefault();\n    setImgCode({\n      value: evt.target.value || '',\n      isInvalid: false,\n      errorMsg: '',\n    });\n  };\n\n  const getCaptcha = () => {\n    const rc = refCaptcha.current;\n    const ri = refImgCode.current;\n    const r = {\n      verify: !!rc?.verify,\n      captcha_id: rc?.captcha_id,\n      captcha_code: ri.value,\n    };\n\n    return r;\n  };\n\n  const resolveCaptchaReq = (req: ImgCodeReq) => {\n    const r = getCaptcha();\n    if (r.verify) {\n      req.captcha_code = r.captcha_code;\n      req.captcha_id = r.captcha_id;\n    }\n  };\n\n  const handleSubmit = (evt) => {\n    evt.preventDefault();\n    if (!imgCode.value) {\n      return;\n    }\n\n    if (refCallback.current) {\n      refCallback.current();\n    }\n  };\n\n  useEffect(() => {\n    if (autoInitCaptchaData) {\n      fetchCaptchaData();\n    }\n  }, []);\n\n  useLayoutEffect(() => {\n    refImgCode.current = imgCode;\n    refCaptcha.current = captcha;\n  }, [captcha, imgCode]);\n\n  useEffect(() => {\n    // @ts-ignore\n    refRoot.current.render(\n      <Modal\n        size=\"sm\"\n        title=\"Captcha\"\n        show={stateShow}\n        onHide={() => close()}\n        centered>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{t('title')}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <Form noValidate onSubmit={handleSubmit}>\n            <Form.Group controlId=\"code\" className=\"mb-3\">\n              <div className=\"mb-3 p-2 d-flex align-items-center justify-content-center bg-light rounded-2\">\n                <img\n                  src={captcha?.captcha_img}\n                  alt=\"captcha img\"\n                  width=\"auto\"\n                  height=\"60px\"\n                />\n              </div>\n              <InputGroup>\n                <Form.Control\n                  type=\"text\"\n                  autoComplete=\"off\"\n                  placeholder={t('placeholder')}\n                  isInvalid={imgCode?.isInvalid}\n                  onChange={handleChange}\n                  value={imgCode.value}\n                />\n                <Button\n                  onClick={fetchCaptchaData}\n                  variant=\"outline-secondary\"\n                  title={t('refresh', { keyPrefix: 'btns' })}\n                  style={{\n                    borderTopRightRadius: '0.375rem',\n                    borderBottomRightRadius: '0.375rem',\n                  }}>\n                  <Icon name=\"arrow-repeat\" />\n                </Button>\n\n                <Form.Control.Feedback type=\"invalid\">\n                  {imgCode?.errorMsg}\n                </Form.Control.Feedback>\n              </InputGroup>\n            </Form.Group>\n\n            <div className=\"d-grid\">\n              <Button type=\"submit\" disabled={!imgCode.value}>\n                {t('verify', { keyPrefix: 'btns' })}\n              </Button>\n            </div>\n          </Form>\n        </Modal.Body>\n      </Modal>,\n    );\n  });\n\n  const r = {\n    close,\n    show,\n    check: (submitFunc: SubmitCallback) => {\n      if (pending.current) {\n        return false;\n      }\n      refCallback.current = submitFunc;\n      if (captcha?.verify) {\n        show();\n        return false;\n      }\n      return submitFunc();\n    },\n    getCaptcha,\n    resolveCaptchaReq,\n    fetchCaptchaData,\n    handleCaptchaError,\n  };\n\n  return r;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/hooks/useChangePasswordModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState, useRef } from 'react';\nimport { Modal, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport type * as Type from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\nimport { handleFormError } from '@/utils';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface IProps {\n  title?: string;\n  onConfirm?: (formData: any) => Promise<any>;\n}\nconst useChangePasswordModal = (props: IProps = {}) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.new_password_modal',\n  });\n\n  const { title = t('title'), onConfirm } = props;\n  const [visible, setVisibleState] = useState(false);\n  const [userId, setUserId] = useState('');\n  const schema: JSONSchema = {\n    title: t('title'),\n    required: ['password'],\n    properties: {\n      password: {\n        type: 'string',\n        title: t('form.fields.password.label'),\n        description: t('form.fields.password.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    password: {\n      'ui:options': {\n        inputType: 'password',\n        validator: (value) => {\n          const MIN_LENGTH = 8;\n          const MAX_LENGTH = 32;\n          if (value.length < MIN_LENGTH || value.length > MAX_LENGTH) {\n            return t('form.fields.password.msg');\n          }\n          return true;\n        },\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const formRef = useRef<{\n    validator: () => Promise<boolean>;\n  }>(null);\n\n  const onClose = () => {\n    setVisibleState(false);\n  };\n\n  const onShow = (user_id: string) => {\n    setUserId(user_id);\n    setVisibleState(true);\n  };\n\n  const handleSubmit = async (event) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const isValid = await formRef.current?.validator();\n\n    if (!isValid) {\n      return;\n    }\n\n    if (onConfirm instanceof Function) {\n      onConfirm({\n        password: formData.password.value,\n        user_id: userId,\n      })\n        .then(() => {\n          setFormData({\n            password: {\n              value: '',\n              isInvalid: false,\n              errorMsg: '',\n            },\n          });\n          setUserId('');\n          onClose();\n        })\n        .catch((err) => {\n          if (err.isError) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n          }\n        });\n    }\n  };\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={visible} title={title} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <SchemaForm\n            ref={formRef}\n            schema={schema}\n            uiSchema={uiSchema}\n            formData={formData}\n            onChange={handleOnChange}\n            hiddenSubmit\n          />\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useChangePasswordModal;\n"
  },
  {
    "path": "ui/src/hooks/useChangeProfileModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState, useRef } from 'react';\nimport { Modal, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport type * as Type from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\nimport { handleFormError } from '@/utils';\nimport pattern from '@/common/pattern';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface IProps {\n  title?: string;\n  onConfirm?: (formData: any) => Promise<any>;\n}\nconst useChangeProfileModal = (props: IProps = {}, userData) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.edit_profile_modal',\n  });\n\n  const { title = t('title'), onConfirm } = props;\n  const [visible, setVisibleState] = useState(false);\n  const [userId, setUserId] = useState('');\n  const schema: JSONSchema = {\n    title: t('title'),\n    required: ['display_name', 'username', 'email'],\n    properties: {\n      display_name: {\n        type: 'string',\n        title: t('form.fields.display_name.label'),\n        default: userData.display_name,\n      },\n      username: {\n        type: 'string',\n        title: t('form.fields.username.label'),\n        default: userData.username,\n      },\n      email: {\n        type: 'string',\n        title: t('form.fields.email.label'),\n        default: userData.e_mail,\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    display_name: {\n      'ui:options': {\n        inputType: 'text',\n        validator: (value) => {\n          const MIN_LENGTH = 2;\n          const MAX_LENGTH = 30;\n          if (value.length < MIN_LENGTH || value.length > MAX_LENGTH) {\n            return t('form.fields.display_name.msg_range');\n          }\n          return true;\n        },\n      },\n    },\n    username: {\n      'ui:options': {\n        inputType: 'text',\n        validator: (value) => {\n          const MIN_LENGTH = 2;\n          const MAX_LENGTH = 30;\n          if (value.length < MIN_LENGTH || value.length > MAX_LENGTH) {\n            return t('form.fields.username.msg_range');\n          }\n          return true;\n        },\n      },\n    },\n    email: {\n      'ui:options': {\n        inputType: 'email',\n        validator: (value) => {\n          if (value && !pattern.email.test(value)) {\n            return t('form.fields.email.msg_invalid');\n          }\n          return true;\n        },\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const formRef = useRef<{\n    validator: () => Promise<boolean>;\n  }>(null);\n\n  const onClose = () => {\n    setFormData(formData);\n    setVisibleState(false);\n  };\n\n  const onShow = (user_id: string) => {\n    setUserId(user_id);\n    setVisibleState(true);\n  };\n\n  const handleSubmit = async (event) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const isValid = await formRef.current?.validator();\n\n    if (!isValid) {\n      return;\n    }\n\n    if (onConfirm instanceof Function) {\n      onConfirm({\n        display_name: formData.display_name.value,\n        username: formData.username.value,\n        email: formData.email.value,\n        user_id: userId,\n      })\n        .then(() => {\n          setUserId('');\n          onClose();\n        })\n        .catch((err) => {\n          if (err.isError) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n          }\n        });\n    }\n  };\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={visible} title={title} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <SchemaForm\n            ref={formRef}\n            schema={schema}\n            uiSchema={uiSchema}\n            formData={formData}\n            onChange={handleOnChange}\n            hiddenSubmit\n          />\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useChangeProfileModal;\n"
  },
  {
    "path": "ui/src/hooks/useChangeUserRoleModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState } from 'react';\nimport { Modal, Form, Button, FormCheck } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport { getUserRoles, changeUserRole } from '@/services';\nimport { UserRoleItem } from '@/common/interface';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface Props {\n  callback?: () => void;\n}\n\nconst useChangeUserRoleModal = ({ callback }: Props) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.user_role_modal',\n  });\n  const [id, setId] = useState('');\n  const [defaultId, setDefaultId] = useState(-1);\n  const [isInvalid, setInvalidState] = useState(false);\n  const [changedId, setChangeId] = useState(-1);\n  const [show, setShow] = useState(false);\n  const [list, setList] = useState<UserRoleItem[]>([]);\n\n  const getRolesData = async () => {\n    const res = await getUserRoles();\n    setList(res);\n  };\n\n  const handleRadio = (val) => {\n    setInvalidState(false);\n    setChangeId(val.id);\n  };\n\n  const onClose = () => {\n    setChangeId(-1);\n    setDefaultId(-1);\n    setShow(false);\n  };\n\n  const handleSubmit = () => {\n    if (defaultId === changedId) {\n      onClose();\n\n      return;\n    }\n\n    changeUserRole({\n      user_id: id,\n      role_id: changedId,\n    }).then(() => {\n      callback?.();\n      onClose();\n    });\n  };\n\n  const onShow = (params) => {\n    getRolesData();\n    setId(params.id);\n    setChangeId(params.role_id);\n    setDefaultId(params.role_id);\n    setShow(true);\n  };\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={show} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{t('title')}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <Form>\n            {list.map((item) => {\n              return (\n                <div key={item?.id}>\n                  <Form.Group controlId={item.name} className=\"mb-3\">\n                    <FormCheck>\n                      <FormCheck.Input\n                        id={item.name}\n                        type=\"radio\"\n                        checked={changedId === item.id}\n                        onChange={() => handleRadio(item)}\n                        isInvalid={isInvalid}\n                      />\n                      <FormCheck.Label htmlFor={item.name}>\n                        <span className=\"fw-bold\">{item.name}</span>\n                        <br />\n                        <span className=\"text-secondary\">\n                          {item.description}\n                        </span>\n                      </FormCheck.Label>\n                      <Form.Control.Feedback type=\"invalid\">\n                        {t('msg.empty')}\n                      </Form.Control.Feedback>\n                    </FormCheck>\n                  </Form.Group>\n                </div>\n              );\n            })}\n          </Form>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useChangeUserRoleModal;\n"
  },
  {
    "path": "ui/src/hooks/useExternalToast/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState } from 'react';\nimport { Toast, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport { EXTERNAL_CONTENT_DISPLAY_MODE } from '@/common/constants';\nimport { Storage } from '@/utils';\n\nconst toastPortal = document.createElement('div');\ntoastPortal.style.position = 'fixed';\ntoastPortal.style.top = '78px';\ntoastPortal.style.left = '50%';\ntoastPortal.style.transform = 'translate(-50%, 0)';\ntoastPortal.style.maxWidth = '100%';\ntoastPortal.style.zIndex = '1001';\n\nconst setPortalPosition = () => {\n  const header = document.querySelector('#header');\n  if (header) {\n    toastPortal.style.top = `${header.getBoundingClientRect().top + 78}px`;\n  }\n};\nconst startHandlePortalPosition = () => {\n  setPortalPosition();\n  window.addEventListener('scroll', setPortalPosition);\n};\n\nconst stopHandlePortalPosition = () => {\n  setPortalPosition();\n  window.removeEventListener('scroll', setPortalPosition);\n};\n\nconst root = ReactDOM.createRoot(toastPortal);\n\nconst useExternalToast = () => {\n  const [show, setShow] = useState(false);\n  const { t } = useTranslation('translation', { keyPrefix: 'messages' });\n\n  const onClose = () => {\n    const parent = document.querySelector('.page-wrap');\n    if (parent?.contains(toastPortal)) {\n      parent.removeChild(toastPortal);\n    }\n    stopHandlePortalPosition();\n    setShow(false);\n  };\n\n  const onShow = () => {\n    startHandlePortalPosition();\n    setShow(true);\n  };\n\n  const showExternalResourceMode = (mode) => {\n    if (mode === 'always') {\n      Storage.set(EXTERNAL_CONTENT_DISPLAY_MODE, 'always');\n    } else {\n      Storage.remove(EXTERNAL_CONTENT_DISPLAY_MODE);\n    }\n    const img = document.querySelectorAll('img');\n    img.forEach((i) => {\n      if (!i.src && i.dataset.src) {\n        i.src = i.dataset.src;\n        i.removeAttribute('data-src');\n        i.classList.remove('broken');\n      }\n    });\n    onClose();\n  };\n\n  useLayoutEffect(() => {\n    const parent = document.querySelector('.page-wrap');\n    parent?.appendChild(toastPortal);\n\n    root.render(\n      <div className=\"d-flex justify-content-center\">\n        <Toast\n          className=\"align-items-center border-0\"\n          bg=\"warning\"\n          show={show}\n          onClose={onClose}>\n          <div className=\"d-flex\">\n            <Toast.Body>\n              {t('external_content_warning')}\n              <div className=\"d-flex align-items-center\">\n                <Button\n                  variant=\"link\"\n                  onClick={() => showExternalResourceMode('below')}\n                  className=\"btn-no-border small link-dark p-0 fw-bold\">\n                  {t('display_below', { keyPrefix: 'btns' })}\n                </Button>\n                <span className=\"mx-1\">{t('or', { keyPrefix: 'btns' })}</span>\n                <Button\n                  variant=\"link\"\n                  onClick={() => showExternalResourceMode('always')}\n                  className=\"btn-no-border small link-dark p-0 fw-bold\">\n                  {t('always_display', { keyPrefix: 'btns' })}\n                </Button>\n              </div>\n            </Toast.Body>\n            <button\n              className=\"btn-close me-2 m-auto\"\n              onClick={onClose}\n              data-bs-dismiss=\"toast\"\n              aria-label=\"Close\"\n            />\n          </div>\n        </Toast>\n      </div>,\n    );\n  }, [show]);\n\n  return {\n    onShow,\n  };\n};\n\nexport default useExternalToast;\n"
  },
  {
    "path": "ui/src/hooks/useLoginRedirect/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useNavigate } from 'react-router-dom';\n\nimport { floppyNavigation } from '@/utils';\nimport Storage from '@/utils/storage';\nimport { RouteAlias } from '@/router/alias';\nimport { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';\n\nconst Index = () => {\n  const navigate = useNavigate();\n\n  const loginRedirect = () => {\n    const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;\n    Storage.remove(REDIRECT_PATH_STORAGE_KEY);\n    floppyNavigation.navigate(redirect, {\n      handler: navigate,\n      options: {\n        replace: true,\n      },\n    });\n  };\n\n  return { loginRedirect };\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/hooks/usePageTags/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect } from 'react';\n\nimport { HelmetUpdate } from '@/common/interface';\nimport { pageTagStore } from '@/stores';\n\nexport default function usePageTags(info: HelmetUpdate) {\n  const { update } = pageTagStore.getState();\n  useEffect(() => {\n    update(info);\n  }, [info.title, info.subtitle, info.description, info.keywords]);\n}\n"
  },
  {
    "path": "ui/src/hooks/usePageUsers/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\n\nimport uniqBy from 'lodash/uniqBy';\n\nimport * as Types from '@/common/interface';\n\nlet globalUsers: Types.PageUser[] = [];\nconst usePageUsers = () => {\n  const [users, setUsers] = useState<Types.PageUser[]>(globalUsers);\n  const getUsers = () => {\n    return users;\n  };\n  return {\n    getUsers,\n    setUsers: (data: Types.PageUser | Types.PageUser[]) => {\n      if (data instanceof Array) {\n        if (data.length === 0) {\n          return;\n        }\n        setUsers(uniqBy([...users, ...data], 'userName'));\n        globalUsers = uniqBy([...globalUsers, ...data], 'userName');\n      } else {\n        setUsers(uniqBy([...users, data], 'userName'));\n        globalUsers = uniqBy([...globalUsers, data], 'userName');\n      }\n    },\n  };\n};\n\nexport default usePageUsers;\n"
  },
  {
    "path": "ui/src/hooks/usePrompt/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useCallback } from 'react';\nimport {\n  useBeforeUnload,\n  unstable_usePrompt as usePrompt,\n} from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\n// https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6\n// The link above is an example of implementing usePrompt with useBlocker.\ninterface PromptProps {\n  when: boolean;\n  beforeUnload?: boolean;\n}\n\nconst usePromptWithUnload = ({\n  when = false,\n  beforeUnload = true,\n}: PromptProps) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'prompt' });\n\n  usePrompt({\n    when,\n    message: `${t('leave_page')} ${t('changes_not_save')}`,\n  });\n\n  useBeforeUnload(\n    useCallback(\n      (event) => {\n        if (beforeUnload && when) {\n          const msg = t('changes_not_save');\n          event.preventDefault();\n          event.returnValue = msg;\n        }\n      },\n      [when, beforeUnload],\n    ),\n    { capture: true },\n  );\n};\n\nexport default usePromptWithUnload;\n"
  },
  {
    "path": "ui/src/hooks/useReportModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useRef, useEffect, useLayoutEffect } from 'react';\nimport { Modal, Form, Button, FormCheck } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport { useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport type * as Type from '@/common/interface';\nimport {\n  reportList,\n  postReport,\n  closeQuestion,\n  putReport,\n  putFlagReviewAction,\n} from '@/services';\n\ninterface Params {\n  isBackend?: boolean;\n  type: Type.ReportType;\n  id: string;\n  title?: string;\n  action: Type.ReportAction;\n  source?: string;\n  content?: string;\n  reportType?: any;\n}\n\nconst useReportModal = (callback?: () => void) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'report_modal' });\n  const toast = useToast();\n  const [params, setParams] = useState<Params | null>(null);\n  const [isInvalid, setInvalidState] = useState(false);\n  const [reportType, setReportType] = useState({\n    type: -1,\n    haveContent: false,\n  });\n  const rootRef = useRef<{ root: ReactDOM.Root | null }>({\n    root: null,\n  });\n\n  const [content, setContent] = useState({\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  });\n  const [show, setShow] = useState(false);\n  const [list, setList] = useState<any[]>([]);\n\n  const rCaptcha = useCaptchaPlugin('report');\n\n  useEffect(() => {\n    const div = document.createElement('div');\n    rootRef.current.root = ReactDOM.createRoot(div);\n  }, []);\n  const getList = ({ type, action, isBackend, ...otherParams }: Params) => {\n    // @ts-ignore\n    reportList({\n      type,\n      action,\n      isBackend,\n    }).then((res) => {\n      setList(res);\n      if (otherParams.reportType) {\n        const findType = res.find(\n          (v) => v.reason_type === otherParams.reportType,\n        );\n        if (findType) {\n          setReportType({\n            type: findType.reason_type,\n            haveContent: Boolean(findType.content_type),\n          });\n\n          setContent({\n            value: otherParams.content || '',\n            isInvalid: false,\n            errorMsg: '',\n          });\n        }\n      }\n      setShow(true);\n    });\n  };\n  const asyncCallback = () => {\n    setTimeout(() => {\n      callback?.();\n    });\n  };\n  const handleRadio = (val) => {\n    setInvalidState(false);\n    setContent({\n      value:\n        val.reason_type === params?.reportType ? String(params?.content) : '',\n      isInvalid: false,\n      errorMsg: '',\n    });\n    setReportType({\n      type: val.reason_type,\n      haveContent: Boolean(val.content_type),\n    });\n  };\n\n  const onClose = () => {\n    setReportType({\n      type: -1,\n      haveContent: false,\n    });\n    setContent({\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    });\n    setShow(false);\n  };\n\n  const checkValidate = () => {\n    if (reportType.haveContent && !content.value) {\n      setContent({\n        value: content.value,\n        isInvalid: true,\n        errorMsg: t('remark.empty'),\n      });\n      return false;\n    }\n\n    if (reportType.type === 60) {\n      // a duplicate\n      let url: URL | undefined;\n      try {\n        url = new URL(content.value);\n      } catch {\n        setContent({\n          value: content.value,\n          isInvalid: true,\n          errorMsg: t('msg.not_a_url'),\n        });\n      }\n      if (!url) return false;\n\n      if (url.origin !== window.location.origin) {\n        setContent({\n          value: content.value,\n          isInvalid: true,\n          errorMsg: t('msg.url_not_match'),\n        });\n        return false;\n      }\n    }\n\n    return true;\n  };\n\n  const submitReport = (data) => {\n    const flagReq = {\n      source: data.type,\n      report_type: reportType.type,\n      object_id: data.id,\n      content: content.value,\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    rCaptcha?.resolveCaptchaReq(flagReq);\n\n    postReport(flagReq)\n      .then(async () => {\n        await rCaptcha?.close();\n        toast.onShow({\n          msg: t('flag_success', { keyPrefix: 'toast' }),\n          variant: 'warning',\n        });\n        onClose();\n        asyncCallback();\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          rCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n\n  const handleSubmit = () => {\n    if (!params) {\n      return;\n    }\n    if (reportType.type === -1) {\n      setInvalidState(true);\n      return;\n    }\n\n    if (!checkValidate()) {\n      return;\n    }\n\n    if (params.type === 'question' && params.action === 'close') {\n      if (params?.source === 'review') {\n        putFlagReviewAction({\n          flag_id: params.id,\n          operation_type: 'close_post',\n          close_type: reportType.type,\n          close_msg: content.value,\n        }).then(() => {\n          onClose();\n          asyncCallback();\n        });\n        return;\n      }\n      closeQuestion({\n        id: params.id,\n        close_type: reportType.type,\n        close_msg: content.value,\n      }).then(() => {\n        onClose();\n        asyncCallback();\n      });\n      return;\n    }\n    if (!params.isBackend && params.action === 'flag') {\n      if (!rCaptcha) {\n        submitReport(params);\n        return;\n      }\n      rCaptcha.check(() => {\n        submitReport(params);\n      });\n    }\n\n    if (params.isBackend && params.action === 'review') {\n      putReport({\n        action: params.type,\n        flagged_content: content.value,\n        flagged_type: reportType.type,\n        id: params.id,\n      }).then(() => {\n        onClose();\n        asyncCallback();\n      });\n    }\n  };\n\n  const onShow = (obj: Params) => {\n    setParams(obj);\n    getList(obj);\n  };\n  let title = '';\n  if (typeof params === 'object' && params) {\n    title = params.title || t(`${params.action}_title`);\n    if (params.action === 'review') {\n      title = t(`${params.action}_${params.type}_title`);\n    }\n  }\n  useLayoutEffect(() => {\n    rootRef.current.root?.render(\n      <Modal show={show} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <Form>\n            {list.map((item) => {\n              return (\n                <div key={item?.reason_type}>\n                  <Form.Group\n                    controlId={`report_${item?.reason_type}`}\n                    className={`${\n                      item.have_content && reportType === item.type\n                        ? 'mb-2'\n                        : 'mb-3'\n                    }`}>\n                    <FormCheck>\n                      <FormCheck.Input\n                        id={item.reason_type}\n                        type=\"radio\"\n                        checked={reportType.type === item.reason_type}\n                        onChange={() => handleRadio(item)}\n                        isInvalid={isInvalid}\n                      />\n                      <FormCheck.Label htmlFor={item.reason_type}>\n                        <span className=\"fw-bold\">{item?.name}</span>\n                        <br />\n                        <span className=\"text-secondary\">\n                          {item?.description}\n                        </span>\n                      </FormCheck.Label>\n                      <Form.Control.Feedback type=\"invalid\">\n                        {t('msg.empty')}\n                      </Form.Control.Feedback>\n                    </FormCheck>\n                  </Form.Group>\n                  {reportType.haveContent &&\n                    reportType.type === item.reason_type && (\n                      <Form.Group controlId=\"content\" className=\"ps-4 mb-3\">\n                        <Form.Control\n                          type=\"text\"\n                          as={\n                            item.content_type === 'text' ? 'input' : 'textarea'\n                          }\n                          value={content.value}\n                          isInvalid={content.isInvalid}\n                          placeholder={item.placeholder}\n                          onChange={(e) =>\n                            setContent({\n                              value: e.target.value,\n                              isInvalid: false,\n                              errorMsg: '',\n                            })\n                          }\n                        />\n                        <Form.Control.Feedback type=\"invalid\">\n                          {content.errorMsg}\n                        </Form.Control.Feedback>\n                      </Form.Group>\n                    )}\n                </div>\n              );\n            })}\n          </Form>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useReportModal;\n"
  },
  {
    "path": "ui/src/hooks/useSkeletonControl/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { SKELETON_SHOW_TIME } from '@/common/constants';\n\n/**\n * @param needShowFirst whether the skeleton should show at first\n *\n * Why need 'needShowFirst' param?\n * Sometimes we need skeleton screens to take up space in the dom from the start\n *\n * If you set the 'needShowFirst' param as false, If the interface time is too short,\n * the skeleton screen will not be displayed, which can reduce the time occupation\n */\nconst useSkeletonControl = (isLoading: boolean) => {\n  const [isSkeletonShow, setIsSkeletonShow] = useState(false);\n  const timer = useRef<NodeJS.Timeout | null>(null);\n  const openSkeleton = () => {\n    if (timer.current) {\n      clearTimeout(timer.current);\n    }\n    timer.current = setTimeout(() => {\n      setIsSkeletonShow(true);\n    }, SKELETON_SHOW_TIME);\n  };\n\n  const closeSkeleton = () => {\n    clearTimeout(timer.current as NodeJS.Timeout);\n    setIsSkeletonShow(false);\n  };\n\n  useEffect(() => {\n    if (isLoading) {\n      openSkeleton();\n    } else {\n      closeSkeleton();\n    }\n  }, [isLoading]);\n\n  return { isSkeletonShow };\n};\n\nexport default useSkeletonControl;\n"
  },
  {
    "path": "ui/src/hooks/useTagModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState } from 'react';\nimport { Modal, Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport { TAG_SLUG_NAME_MAX_LENGTH } from '@/common/constants';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface IProps {\n  title?: string;\n  onConfirm?: (formData: any) => void;\n}\nconst useTagModal = (props: IProps = {}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'tag_modal' });\n\n  const { title = t('title'), onConfirm } = props;\n  const [visible, setVisibleState] = useState(false);\n  const [formData, setFormData] = useState({\n    displayName: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    slugName: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    description: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const onClose = () => {\n    setVisibleState(false);\n  };\n\n  const onShow = (searchStr = '') => {\n    setVisibleState(true);\n    setFormData({\n      ...formData,\n      displayName: {\n        value: searchStr,\n        isInvalid: false,\n        errorMsg: '',\n      },\n      slugName: {\n        value: searchStr,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { displayName, slugName } = formData;\n    if (!displayName.value) {\n      bol = false;\n      formData.displayName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.empty'),\n      };\n    } else if (displayName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.range'),\n      };\n    } else {\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    if (!slugName.value) {\n      bol = false;\n      formData.slugName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.empty'),\n      };\n    } else if (slugName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.range'),\n      };\n      // } else if (/[^a-z0-9+#\\-.]/.test(slugName.value)) {\n      //   bol = false;\n      //   formData.slugName = {\n      //     value: slugName.value,\n      //     isInvalid: true,\n      //     errorMsg: t('form.fields.slug_name.msg.character'),\n      //   };\n    } else {\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n    return bol;\n  };\n\n  const handleSubmit = (event: React.MouseEvent<HTMLElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (onConfirm instanceof Function) {\n      onConfirm({\n        slug_name: formData.slugName.value,\n        display_name: formData.displayName.value,\n        original_text: formData.description.value,\n      });\n      setFormData({\n        displayName: {\n          value: '',\n          isInvalid: false,\n          errorMsg: '',\n        },\n        slugName: {\n          value: '',\n          isInvalid: false,\n          errorMsg: '',\n        },\n        description: {\n          value: '',\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n    }\n    onClose();\n  };\n\n  const handleDisplayNameChange = (\n    event: React.ChangeEvent<HTMLInputElement>,\n  ) => {\n    const { value } = event.target;\n    setFormData({\n      ...formData,\n      displayName: {\n        value,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const handleSlugNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const { value } = event.target;\n    setFormData({\n      ...formData,\n      slugName: {\n        value,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const handleDescriptionChange = (\n    event: React.ChangeEvent<HTMLInputElement>,\n  ) => {\n    const { value } = event.target;\n    setFormData({\n      ...formData,\n      description: {\n        value,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={visible} title={title} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <Form>\n            <Form.Group controlId=\"displayName\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.display_name.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.displayName.value}\n                onChange={handleDisplayNameChange}\n                isInvalid={formData.displayName.isInvalid}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.displayName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"slugName\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.slug_name.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.slugName.value}\n                onChange={handleSlugNameChange}\n                isInvalid={formData.slugName.isInvalid}\n              />\n\n              <Form.Text as=\"div\">\n                {t('form.fields.slug_name.msg.range')}\n              </Form.Text>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.slugName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"description\">\n              <Form.Label>{`${t('form.fields.desc.label')} ${t('optional', {\n                keyPrefix: 'form',\n              })}`}</Form.Label>\n              <Form.Control\n                className=\"font-monospace\"\n                value={formData.description.value}\n                onChange={handleDescriptionChange}\n                as=\"textarea\"\n                rows={2}\n              />\n            </Form.Group>\n          </Form>\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useTagModal;\n"
  },
  {
    "path": "ui/src/hooks/useToast/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState } from 'react';\nimport { Toast } from 'react-bootstrap';\n\nimport ReactDOM from 'react-dom/client';\n\nconst toastPortal = document.createElement('div');\ntoastPortal.style.position = 'fixed';\ntoastPortal.style.top = '78px';\ntoastPortal.style.left = '50%';\ntoastPortal.style.transform = 'translate(-50%, 0)';\ntoastPortal.style.maxWidth = '100%';\ntoastPortal.style.zIndex = '1001';\n\nconst setPortalPosition = () => {\n  const header = document.querySelector('#header');\n  if (header) {\n    toastPortal.style.top = `${header.getBoundingClientRect().top + 78}px`;\n  }\n};\nconst startHandlePortalPosition = () => {\n  setPortalPosition();\n  window.addEventListener('scroll', setPortalPosition);\n};\n\nconst stopHandlePortalPosition = () => {\n  setPortalPosition();\n  window.removeEventListener('scroll', setPortalPosition);\n};\n\nconst root = ReactDOM.createRoot(toastPortal);\n\ninterface Params {\n  /** main content */\n  msg: string;\n  /** theme color */\n  variant?: 'warning' | 'success' | 'danger';\n}\n\nconst useToast = () => {\n  const [show, setShow] = useState(false);\n  const [data, setData] = useState<Params>({\n    msg: '',\n    variant: 'warning',\n  });\n\n  const onClose = () => {\n    const parent = document.querySelector('.page-wrap');\n    if (parent?.contains(toastPortal)) {\n      parent.removeChild(toastPortal);\n    }\n    stopHandlePortalPosition();\n    setShow(false);\n  };\n\n  const onShow = (t: Params) => {\n    setData(t);\n    startHandlePortalPosition();\n    setShow(true);\n  };\n  useLayoutEffect(() => {\n    const parent = document.querySelector('.page-wrap');\n    parent?.appendChild(toastPortal);\n\n    root.render(\n      <div className=\"d-flex justify-content-center\">\n        <Toast\n          className=\"align-items-center border-0\"\n          delay={5000}\n          bg={data.variant || 'warning'}\n          show={show}\n          autohide\n          onClose={onClose}>\n          <div className=\"d-flex\">\n            <Toast.Body\n              dangerouslySetInnerHTML={{ __html: data.msg }}\n              className={`${data.variant !== 'warning' ? 'text-white' : ''}`}\n            />\n            <button\n              className={`btn-close me-2 m-auto ${\n                data.variant !== 'warning' ? 'btn-close-white' : ''\n              }`}\n              onClick={onClose}\n              data-bs-dismiss=\"toast\"\n              aria-label=\"Close\"\n            />\n          </div>\n        </Toast>\n      </div>,\n    );\n  }, [show, data]);\n\n  return {\n    onShow,\n  };\n};\n\nexport default useToast;\n"
  },
  {
    "path": "ui/src/hooks/useUserModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useLayoutEffect, useState, useRef } from 'react';\nimport { Modal, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactDOM from 'react-dom/client';\n\nimport type * as Type from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\nimport { handleFormError } from '@/utils';\n\nconst div = document.createElement('div');\nconst root = ReactDOM.createRoot(div);\n\ninterface IProps {\n  title?: string;\n  onConfirm?: (formData: any) => Promise<any>;\n}\nconst useAddUserModal = (props: IProps = {}) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.user_modal',\n  });\n\n  const { title = t('title'), onConfirm } = props;\n  const [visible, setVisibleState] = useState(false);\n  const schema: JSONSchema = {\n    title: t('title'),\n    required: ['users'],\n    properties: {\n      users: {\n        type: 'string',\n        title: t('form.fields.users.label'),\n        description: t('form.fields.users.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    users: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 7,\n        placeholder: t('form.fields.users.placeholder'),\n        className: 'small font-monospace',\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const formRef = useRef<{\n    validator: () => Promise<boolean>;\n  }>(null);\n\n  const onClose = () => {\n    setVisibleState(false);\n  };\n\n  const onShow = () => {\n    setVisibleState(true);\n  };\n\n  const handleSubmit = async (event) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const isValid = await formRef.current?.validator();\n\n    if (!isValid) {\n      return;\n    }\n\n    if (onConfirm instanceof Function) {\n      onConfirm({\n        users: formData.users.value,\n      })\n        .then(() => {\n          setFormData({\n            users: {\n              value: '',\n              isInvalid: false,\n              errorMsg: '',\n            },\n          });\n          onClose();\n        })\n        .catch((err) => {\n          if (err.isError) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n          }\n        });\n    }\n  };\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  useLayoutEffect(() => {\n    root.render(\n      <Modal show={visible} title={title} onHide={onClose}>\n        <Modal.Header closeButton>\n          <Modal.Title as=\"h5\">{title}</Modal.Title>\n        </Modal.Header>\n        <Modal.Body>\n          <SchemaForm\n            ref={formRef}\n            schema={schema}\n            uiSchema={uiSchema}\n            formData={formData}\n            onChange={handleOnChange}\n            hiddenSubmit\n          />\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"link\" onClick={() => onClose()}>\n            {t('btn_cancel')}\n          </Button>\n          <Button variant=\"primary\" onClick={handleSubmit}>\n            {t('btn_submit')}\n          </Button>\n        </Modal.Footer>\n      </Modal>,\n    );\n  });\n  return {\n    onClose,\n    onShow,\n  };\n};\n\nexport default useAddUserModal;\n"
  },
  {
    "path": "ui/src/i18n/init.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { initReactI18next } from 'react-i18next';\n\nimport i18next from 'i18next';\nimport en_US from '@i18n/en_US.yaml';\n\nimport { DEFAULT_LANG, LANG_RESOURCE_STORAGE_KEY } from '@/common/constants';\nimport Storage from '@/utils/storage';\n\n/**\n * Prevent i18n from re-initialising when the page is refreshed and switching to `fallbackLng`.\n */\nconst initLng = i18next.resolvedLanguage || DEFAULT_LANG;\nconst initResources = {\n  en_US: {\n    translation: en_US.ui,\n  },\n};\n\nconst storageLang = Storage.get(LANG_RESOURCE_STORAGE_KEY);\nif (\n  storageLang &&\n  storageLang.resources &&\n  storageLang.lng &&\n  storageLang.lng !== 'en_US'\n) {\n  initResources[storageLang.lng] = {\n    translation: storageLang.resources,\n  };\n}\n\ni18next\n  //  pass the i18n instance to react-i18next.\n  .use(initReactI18next)\n  .init({\n    resources: initResources,\n    lng: initLng,\n    fallbackLng: DEFAULT_LANG,\n    interpolation: {\n      escapeValue: false,\n    },\n    react: {\n      transSupportBasicHtmlNodes: true,\n      // allow <br/> and simple html elements in translations\n      transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],\n    },\n  });\n\nexport default i18next;\n"
  },
  {
    "path": "ui/src/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n@import 'common/variable';\n@import '~bootstrap/scss/bootstrap';\n@import '~bootstrap-icons';\n@import 'common/color';\n\n.bg-gray-300 {\n  background-color: var(--an-gray-300);\n}\n\n.focus {\n  color: $input-focus-color !important;\n  background-color: $input-focus-bg !important;\n  border-color: $input-focus-border-color !important;\n  outline: 0 !important;\n  @if $enable-shadows {\n    @include box-shadow($input-box-shadow, $input-focus-box-shadow);\n  } @else {\n    box-shadow: $input-focus-box-shadow !important;\n  }\n}\n\nhtml,\nbody {\n  padding: 0;\n}\n\na {\n  text-decoration: none;\n}\n// If the image does not have a `src` attribute, it must break and may not trigger the `onerror` event.\n// With or without the `alt` attribute, it is visually hidden directly.\nimg[src=''] {\n  visibility: hidden !important;\n}\n\n.page-main {\n  min-width: 0;\n  overflow-wrap: break-word;\n  word-break: break-word;\n}\n\n.btn-link {\n  text-decoration: none;\n}\n\n.fs-12 {\n  font-size: 0.75rem;\n}\n\n.small {\n  font-size: 0.875rem;\n}\n\n.pic-auth-modal {\n  width: 25%;\n}\n\n.no-toggle {\n  &::after {\n    display: none;\n  }\n}\n\n.unread-dot {\n  width: 18px;\n  height: 18px;\n  border-radius: 50%;\n  position: absolute;\n  left: 15px;\n  top: 0;\n  border: 1px solid #fff;\n}\n\n.badge-tag {\n  display: inline-block;\n  font-size: 14px;\n  background: $gray-200;\n  padding: 0 7px 1px;\n  color: $gray-700;\n  font-weight: 400;\n  border: 1px solid transparent;\n  &:hover {\n    background: $gray-300;\n  }\n}\n\n.badge-tag-required {\n  font-weight: 700;\n}\n\n.badge-tag-reserved {\n  background: $orange-100;\n  color: $orange-700;\n  border: 1px solid transparent;\n  &:hover {\n    background: $orange-200;\n  }\n}\n\n.divide-line {\n  border-bottom: 1px solid rgba(33, 37, 41, 0.25);\n}\n\n#root {\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n}\n#root footer {\n  margin-top: auto !important;\n  padding-bottom: constant(safe-area-inset-bottom);\n  padding-bottom: env(safe-area-inset-bottom);\n}\n\n.bg-f5 {\n  background-color: var(--an-f5);\n}\n\n.btn-no-border,\n.btn-no-border:hover,\n.btn-no-border:focus,\n.btn-no-border:active:focus {\n  border: none;\n  box-shadow: none;\n}\n\n.pointer:hover {\n  cursor: pointer;\n}\n\n.resize-none {\n  resize: none;\n}\n\n.split-dot {\n  margin-left: 0.25rem !important;\n  margin-right: 0.25rem !important;\n  &:before {\n    content: '\\2022';\n    color: #6c757d;\n  }\n}\n\n.name-ellipsis {\n  display: inline-block;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.text-truncate-1,\n.text-truncate-2,\n.text-truncate-3,\n.text-truncate-4 {\n  display: -webkit-box;\n  overflow: hidden;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 1;\n}\n\n.text-truncate-2 {\n  -webkit-line-clamp: 2;\n}\n.text-truncate-3 {\n  -webkit-line-clamp: 3;\n}\n.text-truncate-4 {\n  -webkit-line-clamp: 4;\n}\n\n.last-p {\n  p:last-child {\n    margin-bottom: 0;\n  }\n}\n\n.me-10 {\n  margin-right: 10px;\n}\n\n.warning {\n  background-color: var(--an-inbox-warning);\n}\n\n.fit-content {\n  height: fit-content;\n  width: fit-content;\n  flex: none;\n}\n\n// fix bug for React-Bootstrap Form.Text\n.form-text {\n  display: inline-block;\n}\n\n.fmt {\n  width: 100%;\n  hr {\n    height: 3px;\n    border: none;\n    position: relative;\n    overflow: visible;\n    text-align: center;\n    opacity: 1;\n    flex: none;\n    margin: 1.5rem 0;\n    &::before {\n      content: '';\n      position: absolute;\n      left: 50%;\n      top: 50%;\n      transform: translate(calc(-50% - 16px), -50%);\n      width: 3px;\n      height: 3px;\n      background-color: var(--bs-border-color);\n      border-radius: 50%;\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      left: 50%;\n      top: 50%;\n      transform: translate(-50%, -50%);\n      width: 3px;\n      height: 3px;\n      background-color: var(--bs-border-color);\n      border-radius: 50%;\n      box-shadow: 16px 0 0 var(--bs-border-color);\n    }\n  }\n  h1 {\n    @extend .fs-3;\n    margin-top: 2rem;\n  }\n  h2 {\n    @extend .fs-4;\n    margin-top: 2rem;\n  }\n  h3 {\n    @extend .fs-5;\n    margin-top: 2rem;\n  }\n  h4 {\n    @extend .fs-6;\n    margin-top: 2rem;\n  }\n  h5 {\n    @extend .small;\n    margin-top: 2rem;\n  }\n  h6 {\n    @extend .fs-12;\n    margin-top: 2rem;\n  }\n  img {\n    max-width: 100%;\n  }\n  video {\n    max-width: 100%;\n  }\n  p {\n    > code {\n      background-color: var(--an-e9ecef);\n      color: var(--an-212529);\n      padding: 2px 4px;\n      border-radius: 0.25rem;\n      overflow-wrap: normal;\n      white-space: inherit;\n    }\n  }\n  pre {\n    background-color: var(--an-pre);\n    border-radius: 0.25rem;\n    padding: 1rem;\n    max-height: 38rem;\n    white-space: pre-wrap;\n    overflow: auto;\n    code {\n      overflow-wrap: normal;\n      white-space: inherit;\n    }\n  }\n  blockquote {\n    border-left: 0.25rem solid #ced4da;\n    padding: 1rem;\n    color: #6c757d;\n    p {\n      color: var(--bs-body-color);\n    }\n    > p:last-child {\n      margin-bottom: 0;\n    }\n  }\n  table {\n    td,\n    th {\n      word-break: initial;\n    }\n  }\n  ol ol,\n  ol ul,\n  ul ol,\n  ul ul {\n    margin-bottom: 1rem;\n  }\n}\n\n.upload-img-wrap {\n  flex-grow: 1;\n  height: 128px;\n}\n\n.review-text-delete {\n  color: #842029;\n  background-color: #f8d7da;\n  text-decoration: line-through;\n  .review-text-add {\n    text-decoration: none;\n  }\n}\n\n.review-text-add {\n  color: #0f5132;\n  background-color: #d1e7dd;\n  text-decoration: none !important;\n}\n\n.rotate-90-deg {\n  display: inline-block;\n  transform: rotate(90deg);\n  transition: transform 0.2s;\n}\n.rotate-0-deg {\n  display: inline-block;\n  transform: rotate(0deg);\n  transition: transform 0.2s;\n}\n\n.pre-line {\n  white-space: pre-wrap;\n}\n\n.reset-p {\n  p {\n    margin-bottom: 0;\n  }\n}\n\n@keyframes bg-fade-out {\n  0%,\n  25% {\n    background-color: var(--bs-highlight-bg);\n  }\n  100% {\n    background-color: transparent;\n  }\n}\n.bg-fade-out {\n  animation: bg-fade-out 2s ease 0.3s;\n}\n\n.btnSvg,\n.btnSvg:hover {\n  display: inline-block;\n  font-size: 16px;\n  width: 16px;\n  height: 16px;\n  fill: currentColor;\n  vertical-align: -0.125em;\n}\n\n.max-width-200 {\n  max-width: 200px;\n}\n\n.alert-exist {\n  color: var(--an-alert-exist-color);\n}\n\n.bronze {\n  color: #cd7f32;\n}\n.silver {\n  color: #c0c0c0;\n}\n.gold {\n  color: #ffd700;\n}\n\n.view-level1 {\n  color: $orange-600;\n}\n\n.view-level2 {\n  color: $orange-700;\n}\n\n.view-level3 {\n  color: $orange-800;\n}\n\n.mb-12 {\n  margin-bottom: 12px;\n}\n\n.a-code-wrap:hover .a-code-tool {\n  display: block;\n}\n.a-code-tool {\n  position: absolute;\n  top: 0.5rem;\n  right: 0.5rem;\n  font-size: 16px;\n  z-index: 1;\n  display: none;\n  line-height: 1;\n}\n\n.inherit {\n  color: inherit !important;\n}\n"
  },
  {
    "path": "ui/src/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React from 'react';\n\nimport ReactDOM from 'react-dom/client';\n\nimport App from './App';\n\nimport './index.scss';\n\nconst root = ReactDOM.createRoot(\n  document.getElementById('root') as HTMLElement,\n);\n\n/**\n * Uniformly hide broken images\n *  - The `onload` event for elements such as `img` can only be `capture` on `document` (window cannot).\n *  - For images with an empty `src` attribute, sometimes the browser will simply display the broken image without reporting an 'error' event.\n */\nconst handleImgLoad = (evt: Event | UIEvent) => {\n  const { target } = evt;\n\n  if (target === null || !(target instanceof Element)) {\n    return;\n  }\n  if (!/IMG/i.test(target.nodeName)) {\n    return;\n  }\n\n  if (/error/i.test(evt.type)) {\n    target.classList.add('broken');\n    const attrSrc = target.getAttribute('src');\n    const attrAlt = target.getAttribute('alt')?.trim();\n    // Images without the `src` attribute are hidden directly by `css`.\n    // Images with `alt` content are not hidden - the display of the `alt` content is also hidden.\n    if (attrSrc && !attrAlt) {\n      target.classList.add('invisible');\n    }\n  }\n\n  if (/load/i.test(evt.type)) {\n    target.classList.remove('broken', 'invisible');\n  }\n};\n\n/**\n *Automatically jump when the href of a Link component within a matching project is not a front-end route.\n *\n */\nconst handleClickLink = (evt: Event) => {\n  const { target } = evt;\n\n  if (target === null || !(target instanceof Element)) {\n    return;\n  }\n  if (!/A/i.test(target.nodeName)) {\n    return;\n  }\n\n  if (target.getAttribute('href')?.includes('/answer/api/')) {\n    evt.preventDefault();\n    window.location.href = target.getAttribute('href') || '';\n  }\n};\n\ndocument.addEventListener('error', handleImgLoad, true);\ndocument.addEventListener('load', handleImgLoad, true);\ndocument.addEventListener('click', handleClickLink, true);\n\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "ui/src/pages/404/403/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { HttpErrorContent } from '@/components';\n\nconst Index = () => {\n  return <HttpErrorContent httpCode=\"403\" />;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/404/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { HttpErrorContent } from '@/components';\n\nconst Index = () => {\n  return <HttpErrorContent httpCode=\"404\" />;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/50X/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useSearchParams } from 'react-router-dom';\n\nimport { HttpErrorContent } from '@/components';\n\nconst Index = () => {\n  const [searchParams] = useSearchParams();\n  const errMsg = searchParams.get('msg') || '';\n  const title = searchParams.get('title') || '';\n  return (\n    <HttpErrorContent\n      httpCode=\"50X\"\n      title={title}\n      errMsg={errMsg}\n      showErrorCode={!errMsg}\n    />\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/AiAssistant/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal, Icon } from '@/components';\nimport { deleteAdminConversation } from '@/services';\nimport { useToast } from '@/hooks';\n\ninterface Props {\n  id: string;\n  refreshList?: () => void;\n}\nconst ConversationsOperation = ({ id, refreshList }: Props) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.conversations',\n  });\n  const toast = useToast();\n\n  const handleAction = (eventKey: string | null) => {\n    if (eventKey === 'delete') {\n      Modal.confirm({\n        title: t('delete_modal.title'),\n        content: t('delete_modal.content'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          deleteAdminConversation(id).then(() => {\n            refreshList?.();\n            toast.onShow({\n              variant: 'success',\n              msg: t('delete_modal.delete_success'),\n            });\n          });\n        },\n      });\n    }\n  };\n\n  return (\n    <Dropdown onSelect={handleAction}>\n      <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0 lh-1\">\n        <Icon name=\"three-dots-vertical\" title={t('action')} />\n      </Dropdown.Toggle>\n      <Dropdown.Menu align=\"end\">\n        <Dropdown.Item eventKey=\"delete\">\n          {t('delete', { keyPrefix: 'btns' })}\n        </Dropdown.Item>\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default ConversationsOperation;\n"
  },
  {
    "path": "ui/src/pages/Admin/AiAssistant/components/DetailModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Button, Modal } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { BubbleAi, BubbleUser } from '@/components';\nimport { useQueryAdminConversationDetail } from '@/services';\n\ninterface IProps {\n  visible: boolean;\n  id: string;\n  onClose?: () => void;\n}\n\nconst Index: FC<IProps> = ({ visible, id, onClose }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.conversations',\n  });\n\n  const { data: conversationDetail } = useQueryAdminConversationDetail(id);\n\n  const handleClose = () => {\n    onClose?.();\n  };\n  return (\n    <Modal show={visible} size=\"lg\" centered onHide={handleClose}>\n      <Modal.Header closeButton>\n        <div style={{ maxWidth: '85%' }} className=\"text-truncate\">\n          {conversationDetail?.topic}\n        </div>\n      </Modal.Header>\n      <Modal.Body className=\"overflow-y-auto\" style={{ maxHeight: '70vh' }}>\n        {conversationDetail?.records.map((item, index) => {\n          const isLastMessage =\n            index === Number(conversationDetail?.records.length) - 1;\n          return (\n            <div\n              key={`${item.chat_completion_id}-${item.role}`}\n              className={`${isLastMessage ? '' : 'mb-4'}`}>\n              {item.role === 'user' ? (\n                <BubbleUser content={item.content} />\n              ) : (\n                <BubbleAi\n                  canType={false}\n                  chatId={item.chat_completion_id}\n                  isLast={false}\n                  isCompleted\n                  content={item.content}\n                  actionData={{\n                    helpful: item.helpful,\n                    unhelpful: item.unhelpful,\n                  }}\n                />\n              )}\n            </div>\n          );\n        })}\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={handleClose}>\n          {t('close', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Admin/AiAssistant/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { Table, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams } from 'react-router-dom';\n\nimport { BaseUserCard, FormatTime, Pagination, Empty } from '@/components';\nimport { useQueryAdminConversationList } from '@/services';\n\nimport DetailModal from './components/DetailModal';\nimport Action from './components/Action';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.conversations',\n  });\n  const [urlSearchParams] = useSearchParams();\n  const curPage = Number(urlSearchParams.get('page') || '1');\n  const PAGE_SIZE = 20;\n  const [detailModal, setDetailModal] = useState({\n    visible: false,\n    id: '',\n  });\n  const {\n    data: conversations,\n    isLoading,\n    mutate: refreshList,\n  } = useQueryAdminConversationList({\n    page: curPage,\n    page_size: PAGE_SIZE,\n  });\n\n  const handleShowDetailModal = (data) => {\n    setDetailModal({\n      visible: true,\n      id: data.id,\n    });\n  };\n\n  const handleHideDetailModal = () => {\n    setDetailModal({\n      visible: false,\n      id: '',\n    });\n  };\n\n  return (\n    <div className=\"d-flex flex-column flex-grow-1 position-relative\">\n      <h3 className=\"mb-4\">{t('ai_assistant', { keyPrefix: 'nav_menus' })}</h3>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th className=\"min-w-15\">{t('topic')}</th>\n            <th style={{ width: '10%' }}>{t('helpful')}</th>\n            <th style={{ width: '10%' }}>{t('unhelpful')}</th>\n            <th style={{ width: '20%' }}>{t('created')}</th>\n            <th style={{ width: '10%' }} className=\"text-end\">\n              {t('action')}\n            </th>\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {conversations?.list.map((item) => {\n            return (\n              <tr key={item.id}>\n                <td>\n                  <Button\n                    variant=\"link\"\n                    className=\"p-0 text-decoration-none text-truncate max-w-30\"\n                    onClick={() => handleShowDetailModal(item)}>\n                    {item.topic}\n                  </Button>\n                </td>\n                <td>{item.helpful_count}</td>\n                <td>{item.unhelpful_count}</td>\n                <td>\n                  <div className=\"vstack\">\n                    <BaseUserCard data={item.user_info} avatarSize=\"20px\" />\n                    <FormatTime\n                      className=\"small text-secondary\"\n                      time={item.created_at}\n                    />\n                  </div>\n                </td>\n                <td className=\"text-end\">\n                  <Action id={item.id} refreshList={refreshList} />\n                </td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </Table>\n      {!isLoading && Number(conversations?.count) <= 0 && (\n        <Empty>{t('empty')}</Empty>\n      )}\n\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={conversations?.count || 0}\n          pageSize={PAGE_SIZE}\n        />\n      </div>\n      <DetailModal\n        visible={detailModal.visible}\n        id={detailModal.id}\n        onClose={handleHideDetailModal}\n      />\n    </div>\n  );\n};\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/AiSettings/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState, useRef } from 'react';\nimport { Form, InputGroup, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n  getAiConfig,\n  useQueryAiProvider,\n  checkAiConfig,\n  saveAiConfig,\n} from '@/services';\nimport { aiControlStore } from '@/stores';\nimport { handleFormError } from '@/utils';\nimport { useToast } from '@/hooks';\nimport * as Type from '@/common/interface';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.ai_settings',\n  });\n  const toast = useToast();\n  const historyConfigRef = useRef<Type.AiConfig>();\n  // const [historyConfig, setHistoryConfig] = useState<Type.AiConfig>();\n  const { data: aiProviders } = useQueryAiProvider();\n\n  const [formData, setFormData] = useState({\n    enabled: {\n      value: false,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    provider: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    api_host: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    api_key: {\n      value: '',\n      isInvalid: false,\n      isValid: false,\n      errorMsg: '',\n    },\n    model: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n  const [apiHostPlaceholder, setApiHostPlaceholder] = useState('');\n  const [modelsData, setModels] = useState<{ id: string }[]>([]);\n  const [isChecking, setIsChecking] = useState(false);\n\n  const getCurrentProviderData = (provider) => {\n    const findHistoryProvider =\n      historyConfigRef.current?.ai_providers.find(\n        (v) => v.provider === provider,\n      ) || historyConfigRef.current?.ai_providers[0];\n\n    return findHistoryProvider;\n  };\n\n  const checkAiConfigData = (data) => {\n    const params = data || {\n      api_host: formData.api_host.value || apiHostPlaceholder,\n      api_key: formData.api_key.value,\n    };\n    setIsChecking(true);\n\n    checkAiConfig(params)\n      .then((res) => {\n        setModels(res);\n        const findHistoryProvider = getCurrentProviderData(\n          formData.provider.value,\n        );\n\n        setIsChecking(false);\n\n        if (!data) {\n          setFormData({\n            ...formData,\n            api_key: {\n              ...formData.api_key,\n              errorMsg: t('api_key.check_success'),\n              isInvalid: false,\n              isValid: true,\n            },\n            model: {\n              value: findHistoryProvider?.model || res[0].id,\n              errorMsg: '',\n              isInvalid: false,\n            },\n          });\n        }\n      })\n      .catch((err) => {\n        console.error('Checking AI config:', err);\n        setIsChecking(false);\n      });\n  };\n\n  const handleProviderChange = (value) => {\n    const findHistoryProvider = getCurrentProviderData(value);\n    setFormData({\n      ...formData,\n      provider: {\n        value,\n        isInvalid: false,\n        errorMsg: '',\n      },\n      api_host: {\n        value: findHistoryProvider?.api_host || '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n      api_key: {\n        value: findHistoryProvider?.api_key || '',\n        isInvalid: false,\n        isValid: false,\n        errorMsg: '',\n      },\n      model: {\n        value: findHistoryProvider?.model || '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n    const provider = aiProviders?.find((item) => item.name === value);\n    const host = findHistoryProvider?.api_host || provider?.default_api_host;\n    if (findHistoryProvider?.model) {\n      checkAiConfigData({\n        api_host: host,\n        api_key: findHistoryProvider.api_key,\n      });\n    } else {\n      setModels([]);\n    }\n  };\n\n  const handleValueChange = (value) => {\n    setFormData((prev) => ({\n      ...prev,\n      ...value,\n    }));\n  };\n\n  const checkValidate = () => {\n    let bol = true;\n\n    const { api_host, api_key, model } = formData;\n\n    if (!api_host.value) {\n      bol = false;\n      formData.api_host = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('api_host.msg'),\n      };\n    }\n\n    if (!api_key.value) {\n      bol = false;\n      formData.api_key = {\n        value: '',\n        isInvalid: true,\n        isValid: false,\n        errorMsg: t('api_key.msg'),\n      };\n    }\n\n    if (!model.value) {\n      bol = false;\n      formData.model = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('model.msg'),\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n\n    return bol;\n  };\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    if (!checkValidate()) {\n      return;\n    }\n    const newProviders = historyConfigRef.current?.ai_providers.map((v) => {\n      if (v.provider === formData.provider.value) {\n        return {\n          provider: formData.provider.value,\n          api_host: formData.api_host.value,\n          api_key: formData.api_key.value,\n          model: formData.model.value,\n        };\n      }\n      return v;\n    });\n\n    const params = {\n      enabled: formData.enabled.value,\n      chosen_provider: formData.provider.value,\n      ai_providers: newProviders,\n    };\n    saveAiConfig(params)\n      .then(() => {\n        aiControlStore.getState().update({\n          ai_enabled: formData.enabled.value,\n        });\n\n        historyConfigRef.current = {\n          ...params,\n          ai_providers: params.ai_providers || [],\n        };\n\n        toast.onShow({\n          msg: t('add_success'),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        const data = handleFormError(err, formData);\n        setFormData({ ...data });\n        const ele = document.getElementById(err.list[0].error_field);\n        ele?.scrollIntoView({ behavior: 'smooth', block: 'center' });\n      });\n  };\n\n  const getAiConfigData = async () => {\n    const aiConfig = await getAiConfig();\n    historyConfigRef.current = aiConfig;\n\n    const currentAiConfig = getCurrentProviderData(aiConfig.chosen_provider);\n    if (currentAiConfig?.model) {\n      const provider = aiProviders?.find(\n        (item) => item.name === formData.provider.value,\n      );\n      const host = currentAiConfig.api_host || provider?.default_api_host;\n      checkAiConfigData({\n        api_host: host,\n        api_key: currentAiConfig.api_key,\n      });\n    }\n\n    setFormData({\n      enabled: {\n        value: aiConfig.enabled || false,\n        isInvalid: false,\n        errorMsg: '',\n      },\n      provider: {\n        value: currentAiConfig?.provider || '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n      api_host: {\n        value: currentAiConfig?.api_host || '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n      api_key: {\n        value: currentAiConfig?.api_key || '',\n        isInvalid: false,\n        isValid: false,\n        errorMsg: '',\n      },\n      model: {\n        value: currentAiConfig?.model || '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  useEffect(() => {\n    getAiConfigData();\n  }, []);\n\n  useEffect(() => {\n    if (formData.provider.value) {\n      const provider = aiProviders?.find(\n        (item) => item.name === formData.provider.value,\n      );\n      if (provider) {\n        setApiHostPlaceholder(provider.default_api_host || '');\n      }\n    }\n    if (!formData.provider.value && aiProviders) {\n      setFormData((prev) => ({\n        ...prev,\n        provider: {\n          ...prev.provider,\n          value: aiProviders[0].name,\n        },\n      }));\n    }\n  }, [aiProviders, formData]);\n\n  return (\n    <div>\n      <h3 className=\"mb-4\">{t('ai_settings', { keyPrefix: 'nav_menus' })}</h3>\n      <div className=\"max-w-748\">\n        <Form noValidate onSubmit={handleSubmit}>\n          <Form.Group className=\"mb-3\" controlId=\"enabled\">\n            <Form.Label>{t('enabled.label')}</Form.Label>\n            <Form.Switch\n              type=\"switch\"\n              id=\"enabled\"\n              label={t('enabled.check')}\n              checked={formData.enabled.value}\n              onChange={(e) =>\n                handleValueChange({\n                  enabled: {\n                    value: e.target.checked,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                })\n              }\n            />\n            <Form.Text className=\"text-muted\">{t('enabled.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.enabled.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"provider\">\n            <Form.Label>{t('provider.label')}</Form.Label>\n            <Form.Select\n              isInvalid={formData.provider.isInvalid}\n              value={formData.provider.value}\n              onChange={(e) => handleProviderChange(e.target.value)}>\n              {aiProviders?.map((provider) => (\n                <option key={provider.name} value={provider.name}>\n                  {provider.display_name}\n                </option>\n              ))}\n            </Form.Select>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.provider.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"api_host\">\n            <Form.Label>{t('api_host.label')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              autoComplete=\"off\"\n              placeholder={apiHostPlaceholder}\n              isInvalid={formData.api_host.isInvalid}\n              value={formData.api_host.value}\n              onChange={(e) =>\n                handleValueChange({\n                  api_host: {\n                    value: e.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.api_host.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"api_key\">\n            <Form.Label>{t('api_key.label')}</Form.Label>\n            <InputGroup>\n              <Form.Control\n                type=\"password\"\n                autoComplete=\"new-password\"\n                isInvalid={formData.api_key.isInvalid}\n                isValid={formData.api_key.isValid}\n                value={formData.api_key.value}\n                onChange={(e) =>\n                  handleValueChange({\n                    api_key: {\n                      value: e.target.value,\n                      errorMsg: '',\n                      isInvalid: false,\n                      isValid: false,\n                    },\n                  })\n                }\n              />\n              <Button\n                variant=\"outline-secondary\"\n                className=\"rounded-end\"\n                onClick={() => checkAiConfigData(null)}\n                disabled={isChecking}>\n                {t('api_key.check')}\n              </Button>\n              <Form.Control.Feedback\n                type={formData.api_key.isValid ? 'valid' : 'invalid'}>\n                {formData.api_key.errorMsg}\n              </Form.Control.Feedback>\n            </InputGroup>\n          </Form.Group>\n\n          <div className=\"mb-3\">\n            <label htmlFor=\"model\" className=\"form-label\">\n              {t('model.label')}\n            </label>\n            {/* <Form.Select\n            list=\"datalistOptions\"\n            isInvalid={formData.model.isInvalid}\n            value={formData.model.value}\n            onChange={(e) =>\n              handleValueChange({\n                model: {\n                  value: e.target.value,\n                  errorMsg: '',\n                  isInvalid: false,\n                },\n              })\n            }>\n            {modelsData?.map((model) => {\n              return (\n                <option key={model.id} value={model.id}>\n                  {model.id}\n                </option>\n              );\n            })}\n          </Form.Select> */}\n            <input\n              className=\"form-control\"\n              list=\"datalistOptions\"\n              id=\"model\"\n              value={formData.model.value}\n              onChange={(e) =>\n                handleValueChange({\n                  model: {\n                    value: e.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                })\n              }\n            />\n            <datalist id=\"datalistOptions\">\n              {modelsData?.map((model) => {\n                return (\n                  <option key={model.id} value={model.id}>\n                    {model.id}\n                  </option>\n                );\n              })}\n            </datalist>\n\n            <div className=\"invalid-feedback\">{formData.model.errorMsg}</div>\n          </div>\n\n          <Button type=\"submit\">{t('save', { keyPrefix: 'btns' })}</Button>\n        </Form>\n      </div>\n    </div>\n  );\n};\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Answers/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { Icon, Modal } from '@/components';\nimport { changeAnswerStatus } from '@/services';\nimport { toastStore } from '@/stores';\n\nconst AnswerActions = ({ itemData, curFilter, refreshList }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'delete' });\n\n  const handleAction = (type) => {\n    if (type === 'delete') {\n      Modal.confirm({\n        title: t('title'),\n        content: itemData.accepted === 2 ? t('answer_accepted') : t('other'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          changeAnswerStatus(itemData.id, 'deleted').then(() => {\n            toastStore.getState().show({\n              msg: t('answer_deleted', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n\n    if (type === 'undelete') {\n      Modal.confirm({\n        title: t('undelete_title'),\n        content: t('undelete_desc'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('undelete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          changeAnswerStatus(itemData.id, 'available').then(() => {\n            toastStore.getState().show({\n              msg: t('answer_cancel_deleted', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n  };\n\n  if (curFilter === 'pending') {\n    return (\n      <Link\n        to={`/review?type=queued_post&objectId=${itemData.id}`}\n        className=\"btn btn-link p-0\"\n        title={t('review', { keyPrefix: 'header.nav' })}>\n        <Icon name=\"three-dots-vertical\" />\n      </Link>\n    );\n  }\n\n  return (\n    <Dropdown>\n      <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0\">\n        <Icon\n          name=\"three-dots-vertical\"\n          title={t('action', { keyPrefix: 'admin.answers' })}\n        />\n      </Dropdown.Toggle>\n      <Dropdown.Menu align=\"end\">\n        {curFilter === 'deleted' ? (\n          <Dropdown.Item onClick={() => handleAction('undelete')}>\n            {t('undelete', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        ) : (\n          <Dropdown.Item onClick={() => handleAction('delete')}>\n            {t('delete', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        )}\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default AnswerActions;\n"
  },
  {
    "path": "ui/src/pages/Admin/Answers/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Form, Table, Stack, Button } from 'react-bootstrap';\nimport { useSearchParams, Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport {\n  FormatTime,\n  Icon,\n  Pagination,\n  BaseUserCard,\n  Empty,\n  QueryGroup,\n  Modal,\n  TabNav,\n} from '@/components';\nimport { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';\nimport * as Type from '@/common/interface';\nimport { deletePermanently, useAnswerSearch } from '@/services';\nimport { escapeRemove } from '@/utils';\nimport { pathFactory } from '@/router/pathFactory';\nimport { toastStore } from '@/stores';\n\nimport AnswerAction from './components/Action';\n\nconst answerFilterItems: Type.AdminContentsFilterBy[] = [\n  'normal',\n  'pending',\n  'deleted',\n];\n\nconst Answers: FC = () => {\n  const [urlSearchParams, setUrlSearchParams] = useSearchParams();\n  const curFilter = urlSearchParams.get('status') || answerFilterItems[0];\n  const PAGE_SIZE = 20;\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const curQuery = urlSearchParams.get('query') || '';\n  const questionId = urlSearchParams.get('questionId') || '';\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' });\n\n  const {\n    data: listData,\n    isLoading,\n    mutate: refreshList,\n  } = useAnswerSearch({\n    page_size: PAGE_SIZE,\n    page: curPage,\n    status: curFilter as Type.AdminContentsFilterBy,\n    query: curQuery,\n    question_id: questionId,\n  });\n  const count = listData?.count || 0;\n\n  const handleDeletePermanently = () => {\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'delete_permanently' }),\n      content: t('content', { keyPrefix: 'delete_permanently' }),\n      cancelBtnVariant: 'link',\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      confirmBtnVariant: 'danger',\n      onConfirm: () => {\n        deletePermanently('answers').then(() => {\n          toastStore.getState().show({\n            msg: t('answers_deleted', { keyPrefix: 'messages' }),\n            variant: 'success',\n          });\n          refreshList();\n        });\n      },\n    });\n  };\n\n  const handleFilter = (e) => {\n    urlSearchParams.set('query', e.target.value);\n    urlSearchParams.delete('page');\n    setUrlSearchParams(urlSearchParams);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">\n        {t('page_title', { keyPrefix: 'admin.questions' })}\n      </h3>\n      <TabNav menus={ADMIN_QA_NAV_MENUS} />\n      <div className=\"d-flex flex-wrap justify-content-between align-items-center\">\n        <Stack direction=\"horizontal\" gap={3} className=\"mb-3\">\n          <QueryGroup\n            data={answerFilterItems}\n            currentSort={curFilter}\n            sortKey=\"status\"\n            i18nKeyPrefix=\"btns\"\n          />\n          {curFilter === 'deleted' && count > 0 ? (\n            <Button\n              variant=\"outline-danger\"\n              size=\"sm\"\n              onClick={() => handleDeletePermanently()}>\n              {t('deleted_permanently', { keyPrefix: 'btns' })}\n            </Button>\n          ) : null}\n        </Stack>\n\n        <Form.Control\n          value={curQuery}\n          onChange={handleFilter}\n          size=\"sm\"\n          type=\"search\"\n          placeholder={t('filter.placeholder')}\n          style={{ width: '12.25rem' }}\n          className=\"mb-3\"\n        />\n      </div>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th className=\"min-w-15\">{t('post')}</th>\n            <th style={{ width: '11%' }}>{t('votes')}</th>\n            <th style={{ width: '14%' }}>{t('created')}</th>\n            <th style={{ width: '11%' }}>{t('status')}</th>\n            <th style={{ width: '11%' }} className=\"text-end\">\n              {t('action')}\n            </th>\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {listData?.list?.map((li) => {\n            return (\n              <tr key={li.id}>\n                <td>\n                  <Link\n                    to={pathFactory.answerLanding({\n                      questionId: li.question_id,\n                      slugTitle: li.question_info.url_title,\n                      answerId: li.id,\n                    })}\n                    target=\"_blank\"\n                    className=\"text-break text-wrap\"\n                    rel=\"noreferrer\">\n                    {li.question_info.title}\n                  </Link>\n                  {li.accepted === 2 && (\n                    <Icon\n                      name=\"check-circle-fill\"\n                      className=\"ms-2 text-success\"\n                    />\n                  )}\n                  <div className=\"text-truncate-2 small max-w-30\">\n                    {escapeRemove(li.description)}\n                  </div>\n                </td>\n                <td>{li.vote_count}</td>\n                <td>\n                  <Stack>\n                    <BaseUserCard\n                      avatarSize=\"20\"\n                      data={li.user_info}\n                      nameMaxWidth=\"200px\"\n                    />\n\n                    <FormatTime\n                      className=\"small text-secondary\"\n                      time={li.create_time}\n                    />\n                  </Stack>\n                </td>\n                <td>\n                  <span\n                    className={classNames(\n                      'badge',\n                      ADMIN_LIST_STATUS[curFilter]?.variant,\n                    )}>\n                    {t(ADMIN_LIST_STATUS[curFilter]?.name, {\n                      keyPrefix: 'btns',\n                    })}\n                  </span>\n                </td>\n                <td className=\"text-end\">\n                  <AnswerAction\n                    itemData={{ id: li.id, accepted: li.accepted }}\n                    curFilter={curFilter}\n                    refreshList={refreshList}\n                  />\n                </td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </Table>\n      {Number(count) <= 0 && !isLoading && <Empty />}\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={count}\n          pageSize={PAGE_SIZE}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Answers;\n"
  },
  {
    "path": "ui/src/pages/Admin/Apikeys/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon, Modal } from '@/components';\nimport { deleteApiKey } from '@/services';\nimport { toastStore } from '@/stores';\n\nconst ApiActions = ({ itemData, refreshList, showModal }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.apikeys.delete_modal',\n  });\n\n  const handleAction = (type) => {\n    if (type === 'delete') {\n      Modal.confirm({\n        title: t('title'),\n        content: t('content'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          deleteApiKey(itemData.id).then(() => {\n            toastStore.getState().show({\n              msg: t('api_key_deleted', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n\n    if (type === 'edit') {\n      showModal(true);\n    }\n  };\n\n  return (\n    <Dropdown>\n      <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0\">\n        <Icon\n          name=\"three-dots-vertical\"\n          title={t('action', { keyPrefix: 'admin.answers' })}\n        />\n      </Dropdown.Toggle>\n      <Dropdown.Menu align=\"end\">\n        <Dropdown.Item onClick={() => handleAction('edit')}>\n          {t('edit', { keyPrefix: 'btns' })}\n        </Dropdown.Item>\n        <Dropdown.Item onClick={() => handleAction('delete')}>\n          {t('delete', { keyPrefix: 'btns' })}\n        </Dropdown.Item>\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default ApiActions;\n"
  },
  {
    "path": "ui/src/pages/Admin/Apikeys/components/AddOrEditModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { Modal, Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { handleFormError } from '@/utils';\nimport { addApiKey, updateApiKey } from '@/services';\n\nconst initFormData = {\n  description: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n  scope: {\n    value: 'read-only',\n    isInvalid: false,\n    errorMsg: '',\n  },\n};\n\nconst Index = ({ data, visible = false, onClose, callback }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.apikeys.add_or_edit_modal',\n  });\n  const [formData, setFormData] = useState<any>(initFormData);\n\n  const handleValueChange = (value) => {\n    setFormData({\n      ...formData,\n      ...value,\n    });\n  };\n\n  const handleAdd = () => {\n    const { description, scope } = formData;\n    if (!description.value) {\n      setFormData({\n        ...formData,\n        description: {\n          ...description,\n          isInvalid: true,\n          errorMsg: t('description_required'),\n        },\n      });\n      return;\n    }\n    addApiKey({\n      description: description.value,\n      scope: scope.value,\n    })\n      .then((res) => {\n        callback('add', res.access_key);\n        setFormData(initFormData);\n      })\n      .catch((error) => {\n        const obj = handleFormError(error, formData);\n        setFormData({ ...obj });\n      });\n  };\n\n  const handleEdit = () => {\n    const { description } = formData;\n    if (!description.value) {\n      setFormData({\n        ...formData,\n        description: {\n          ...description,\n          isInvalid: true,\n          errorMsg: t('description_required'),\n        },\n      });\n      return;\n    }\n    updateApiKey({\n      description: description.value,\n      id: data?.id,\n    })\n      .then(() => {\n        callback('edit', null);\n        setFormData(initFormData);\n      })\n      .catch((error) => {\n        const obj = handleFormError(error, formData);\n        setFormData({ ...obj });\n      });\n  };\n\n  const handleSubmit = () => {\n    if (data?.id) {\n      handleEdit();\n      return;\n    }\n    handleAdd();\n  };\n\n  const closeModal = () => {\n    setFormData(initFormData);\n    onClose(false, null);\n  };\n  return (\n    <Modal show={visible} onHide={closeModal}>\n      <Modal.Header closeButton>\n        {data?.id ? t('edit_title') : t('add_title')}\n      </Modal.Header>\n      <Modal.Body>\n        <Form>\n          <Form.Group controlId=\"description\" className=\"mb-3\">\n            <Form.Label>{t('description')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              isInvalid={formData.description.isInvalid}\n              value={formData.description.value}\n              onChange={(e) => {\n                handleValueChange({\n                  description: {\n                    value: e.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.description.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          {!data?.id && visible && (\n            <Form.Group controlId=\"scope\" className=\"mb-3\">\n              <Form.Label>{t('scope')}</Form.Label>\n              <Form.Select\n                isInvalid={formData.scope.isInvalid}\n                value={formData.scope.value}\n                onChange={(e) => {\n                  handleValueChange({\n                    scope: {\n                      value: e.target.value,\n                      errorMsg: '',\n                      isInvalid: false,\n                    },\n                  });\n                }}>\n                <option value=\"read-only\">{t('read-only')}</option>\n                <option value=\"global\">{t('global')}</option>\n              </Form.Select>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.scope.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n          )}\n        </Form>\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={closeModal}>\n          {t('cancel', { keyPrefix: 'btns' })}\n        </Button>\n        <Button type=\"button\" variant=\"primary\" onClick={handleSubmit}>\n          {t('submit', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Apikeys/components/CreatedModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Modal, Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nconst Index = ({ visible, api_key = '', onClose }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.apikeys.created_modal',\n  });\n\n  return (\n    <Modal show={visible} onHide={onClose}>\n      <Modal.Header closeButton>{t('title')}</Modal.Header>\n      <Modal.Body>\n        <Form>\n          <Form.Group controlId=\"api_key\" className=\"mb-3\">\n            <Form.Label>{t('api_key')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              defaultValue={api_key}\n              readOnly\n              disabled\n            />\n          </Form.Group>\n\n          <div className=\"mb-3\">{t('description')}</div>\n        </Form>\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={onClose}>\n          {t('close', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Apikeys/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Action from './Action';\nimport AddOrEditModal from './AddOrEditModal';\nimport CreatedModal from './CreatedModal';\n\nexport { Action, AddOrEditModal, CreatedModal };\n"
  },
  {
    "path": "ui/src/pages/Admin/Apikeys/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Button, Table } from 'react-bootstrap';\n\nimport dayjs from 'dayjs';\n\nimport { useQueryApiKeys } from '@/services';\n\nimport { Action, AddOrEditModal, CreatedModal } from './components';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.apikeys',\n  });\n  const [showModal, setShowModal] = useState({\n    visible: false,\n    item: null,\n  });\n  const [showCreatedModal, setShowCreatedModal] = useState({\n    visible: false,\n    api_key: '',\n  });\n  const { data: apiKeysList, mutate: refreshList } = useQueryApiKeys();\n\n  const handleAddModalState = (bol, item) => {\n    setShowModal({\n      visible: bol,\n      item,\n    });\n  };\n\n  const handleCreatedModalState = (visible, api_key) => {\n    setShowCreatedModal({\n      visible,\n      api_key,\n    });\n  };\n\n  const addOrEditCallback = (type, key) => {\n    handleAddModalState(false, null);\n    refreshList();\n    if (type === 'add') {\n      handleCreatedModalState(true, key);\n    }\n  };\n\n  return (\n    <div>\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <Button\n        variant=\"outline-primary mb-3\"\n        size=\"sm\"\n        onClick={() => handleAddModalState(true, null)}>\n        {t('add_api_key')}\n      </Button>\n      <Table responsive=\"md\">\n        <thead className=\"c-table\">\n          <tr>\n            <th style={{ width: '20%' }}>{t('desc')}</th>\n            <th style={{ width: '11%' }}>{t('scope')}</th>\n            <th style={{ minWidth: '200px' }}>{t('key')}</th>\n            <th style={{ width: '18%' }}>{t('created')}</th>\n            <th style={{ width: '18%' }}>{t('last_used')}</th>\n            <th className=\"text-end\" style={{ width: '10%' }}>\n              {t('action', { keyPrefix: 'admin.questions' })}\n            </th>\n          </tr>\n          {apiKeysList?.map((item) => {\n            return (\n              <tr key={item.id}>\n                <td>{item.description}</td>\n                <td>\n                  {t(item.scope, {\n                    keyPrefix: 'admin.apikeys.add_or_edit_modal',\n                  })}\n                </td>\n                <td>{item.access_key}</td>\n                <td>\n                  {dayjs\n                    .unix(item?.created_at)\n                    .tz()\n                    .format(t('long_date_with_time', { keyPrefix: 'dates' }))}\n                </td>\n                <td>\n                  {item?.last_used_at &&\n                    dayjs\n                      .unix(item?.last_used_at)\n                      .tz()\n                      .format(t('long_date_with_time', { keyPrefix: 'dates' }))}\n                </td>\n                <td className=\"text-end\">\n                  <Action\n                    itemData={item}\n                    showModal={() => handleAddModalState(true, item)}\n                    refreshList={refreshList}\n                  />\n                </td>\n              </tr>\n            );\n          })}\n        </thead>\n      </Table>\n\n      <AddOrEditModal\n        data={showModal.item}\n        visible={showModal.visible}\n        onClose={handleAddModalState}\n        callback={addOrEditCallback}\n      />\n      <CreatedModal\n        visible={showCreatedModal.visible}\n        api_key={showCreatedModal.api_key}\n        onClose={() => handleCreatedModalState(false, '')}\n      />\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Badges/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon } from '@/components';\n\ninterface Props {\n  status: string;\n  onSelect: (eventKey: string | null) => void;\n}\nconst BadgeOperation = ({ onSelect, status }: Props) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' });\n\n  return (\n    <td className=\"text-end\">\n      <Dropdown onSelect={onSelect}>\n        <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0\">\n          <Icon name=\"three-dots-vertical\" title={t('action')} />\n        </Dropdown.Toggle>\n        <Dropdown.Menu align=\"end\">\n          {status === 'inactive' && (\n            <Dropdown.Item eventKey=\"active\">{t('activate')}</Dropdown.Item>\n          )}\n          {status === 'active' && (\n            <Dropdown.Item eventKey=\"inactive\">{t('deactivate')}</Dropdown.Item>\n          )}\n        </Dropdown.Menu>\n      </Dropdown>\n    </td>\n  );\n};\n\nexport default BadgeOperation;\n"
  },
  {
    "path": "ui/src/pages/Admin/Badges/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Form, Table, Stack } from 'react-bootstrap';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { Empty, Icon, Pagination, QueryGroup } from '@/components';\nimport * as Type from '@/common/interface';\nimport { useQueryBadges, updateBadgeStatus } from '@/services/admin/badges';\nimport { useToast } from '@/hooks';\n\nimport Action from './components/Action';\n\nconst BadgeFilterKeys: Type.BadgeFilterBy[] = ['all', 'active', 'inactive'];\n\nconst bgMap = {\n  active: 'text-bg-success',\n  inactive: 'text-bg-secondary',\n};\n\nconst PAGE_SIZE = 10;\n\nconst Badges: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' });\n\n  const [urlSearchParams, setUrlSearchParams] = useSearchParams();\n  const curPage = Number(urlSearchParams.get('page') || '1');\n  const curFilter = urlSearchParams.get('filter') || BadgeFilterKeys[0];\n  const curQuery = urlSearchParams.get('query') || '';\n  const Toast = useToast();\n\n  const { data, isLoading, mutate } = useQueryBadges({\n    page: curPage,\n    page_size: PAGE_SIZE,\n    ...(curQuery ? { q: curQuery } : {}),\n    ...(curFilter === 'all' ? {} : { status: curFilter }),\n  });\n\n  const handleFilter = (e) => {\n    urlSearchParams.set('query', e.target.value);\n    urlSearchParams.delete('page');\n    setUrlSearchParams(urlSearchParams);\n  };\n\n  const handleBadgeStatus = (badgeId, status) => {\n    updateBadgeStatus({ id: badgeId, status }).then(() => {\n      Toast.onShow({\n        msg:\n          status === 'inactive'\n            ? t('badge_inactivated', { keyPrefix: 'messages' })\n            : t('badge_activated', { keyPrefix: 'messages' }),\n        variant: 'success',\n      });\n      mutate();\n    });\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <div className=\"d-flex flex-wrap justify-content-between align-items-center\">\n        <Stack direction=\"horizontal\" gap={3} className=\"mb-3\">\n          <QueryGroup\n            data={BadgeFilterKeys}\n            currentSort={curFilter}\n            sortKey=\"filter\"\n            i18nKeyPrefix=\"admin.badges\"\n          />\n        </Stack>\n\n        <Form.Control\n          size=\"sm\"\n          type=\"search\"\n          value={curQuery}\n          onChange={handleFilter}\n          placeholder={t('filter.placeholder')}\n          style={{ width: '12.25rem' }}\n          className=\"mb-3\"\n        />\n      </div>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th className=\"min-w-15\">{t('name')}</th>\n            <th style={{ width: '19%' }}>{t('group')}</th>\n            <th style={{ width: '14%' }}>{t('awards')}</th>\n            <th style={{ width: '12%' }}>{t('status')}</th>\n            <th className=\"text-end\" style={{ width: '9%' }}>\n              {t('action')}\n            </th>\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {data?.list?.map((badge) => (\n            <tr key={badge.id}>\n              <td className=\"d-flex align-items-center\">\n                {badge.icon?.startsWith('http') ? (\n                  <img\n                    src={badge.icon}\n                    width={32}\n                    height={32}\n                    alt={badge.name}\n                    className=\"me-3\"\n                  />\n                ) : (\n                  <Icon\n                    name={badge?.icon}\n                    size=\"32px\"\n                    className={classNames(\n                      'lh-1 me-3',\n                      badge?.level === 1 && 'bronze',\n                      badge?.level === 2 && 'silver',\n                      badge?.level === 3 && 'gold',\n                    )}\n                  />\n                )}\n                <div>\n                  <Link to={`/badges/${badge.id}`}>{badge.name}</Link>\n                  <div\n                    className=\"text-body small\"\n                    dangerouslySetInnerHTML={{\n                      __html: badge.description,\n                    }}\n                  />\n                </div>\n              </td>\n\n              <td>{badge.group_name}</td>\n              <td>\n                <Link to={`/badges/${badge.id}`}>{badge.award_count}</Link>\n              </td>\n              <td>\n                <span className={classNames('badge', bgMap[badge.status])}>\n                  {t(badge.status)}\n                </span>\n              </td>\n              <Action\n                status={badge.status}\n                onSelect={(status) => handleBadgeStatus(badge.id, status)}\n              />\n            </tr>\n          ))}\n        </tbody>\n      </Table>\n      {Number(data?.count) <= 0 && !isLoading && <Empty />}\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={data?.count || 0}\n          pageSize={PAGE_SIZE}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Badges;\n"
  },
  {
    "path": "ui/src/pages/Admin/Branding/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { JSONSchema, SchemaForm, UISchema, ImgViewer } from '@/components';\nimport { FormDataType } from '@/common/interface';\nimport { brandSetting, getBrandSetting } from '@/services';\nimport { brandingStore } from '@/stores';\nimport { useToast } from '@/hooks';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst uploadType = 'branding';\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.branding',\n  });\n  const { branding: brandingInfo, update } = brandingStore();\n  const Toast = useToast();\n\n  const [formData, setFormData] = useState<FormDataType>({\n    logo: {\n      value: brandingInfo.logo,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    mobile_logo: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    square_icon: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    favicon: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      logo: {\n        type: 'string',\n        title: `${t('logo.label')} ${t('optional', { keyPrefix: 'form' })}`,\n        description: t('logo.text'),\n      },\n      mobile_logo: {\n        type: 'string',\n        title: `${t('mobile_logo.label')} ${t('optional', {\n          keyPrefix: 'form',\n        })}`,\n        description: t('mobile_logo.text'),\n      },\n      square_icon: {\n        type: 'string',\n        title: `${t('square_icon.label')} ${t('optional', {\n          keyPrefix: 'form',\n        })}`,\n        description: t('square_icon.text'),\n      },\n      favicon: {\n        type: 'string',\n        title: `${t('favicon.label')} ${t('optional', {\n          keyPrefix: 'form',\n        })}`,\n        description: t('favicon.text'),\n      },\n    },\n  };\n\n  const uiSchema: UISchema = {\n    logo: {\n      'ui:widget': 'upload',\n      'ui:options': {\n        imageType: uploadType,\n        className: 'object-fit-contain',\n      },\n    },\n    mobile_logo: {\n      'ui:widget': 'upload',\n      'ui:options': {\n        imageType: uploadType,\n        className: 'object-fit-contain',\n      },\n    },\n    square_icon: {\n      'ui:widget': 'upload',\n      'ui:options': {\n        imageType: uploadType,\n        className: 'object-fit-contain',\n      },\n    },\n    favicon: {\n      'ui:widget': 'upload',\n      'ui:options': {\n        acceptType: ',image/x-icon,image/vnd.microsoft.icon',\n        imageType: uploadType,\n        className: 'object-fit-contain',\n      },\n    },\n  };\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  const onSubmit = () => {\n    const params = {\n      logo: formData.logo.value,\n      mobile_logo: formData.mobile_logo.value,\n      square_icon: formData.square_icon.value,\n      favicon: formData.favicon.value,\n    };\n    brandSetting(params)\n      .then(() => {\n        update(params);\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const getBrandData = async () => {\n    const res = await getBrandSetting();\n    if (res) {\n      formData.logo.value = res.logo;\n      formData.mobile_logo.value = res.mobile_logo;\n      formData.square_icon.value = res.square_icon;\n      formData.favicon.value = res.favicon;\n      setFormData({ ...formData });\n    }\n  };\n\n  useEffect(() => {\n    getBrandData();\n  }, []);\n\n  return (\n    <ImgViewer>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onSubmit={onSubmit}\n          onChange={handleOnChange}\n        />\n      </div>\n    </ImgViewer>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Admin/CssAndHtml/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { getPageCustom, putPageCustom } from '@/services';\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport { useToast } from '@/hooks';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { customizeStore } from '@/stores';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.css_and_html',\n  });\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      custom_css: {\n        type: 'string',\n        title: t('custom_css.label'),\n        description: t('custom_css.text'),\n      },\n      custom_head: {\n        type: 'string',\n        title: t('head.label'),\n        description: t('head.text'),\n      },\n      custom_header: {\n        type: 'string',\n        title: t('header.label'),\n        description: t('header.text'),\n      },\n      custom_sidebar: {\n        type: 'string',\n        title: t('sidebar.label'),\n        description: t('sidebar.text'),\n      },\n      custom_footer: {\n        type: 'string',\n        title: t('footer.label'),\n        description: t('footer.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    custom_css: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: ['small', 'font-monospace'],\n      },\n    },\n    custom_head: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: ['small', 'font-monospace'],\n      },\n    },\n    custom_header: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: ['small', 'font-monospace'],\n      },\n    },\n    custom_sidebar: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: ['small', 'font-monospace'],\n      },\n    },\n    custom_footer: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: ['small', 'font-monospace'],\n      },\n    },\n  };\n  const [formData, setFormData] = useState(initFormData(schema));\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: Type.AdminSettingsCustom = {\n      custom_css: formData.custom_css.value,\n      custom_head: formData.custom_head.value,\n      custom_header: formData.custom_header.value,\n      custom_sidebar: formData.custom_sidebar.value,\n      custom_footer: formData.custom_footer.value,\n    };\n\n    putPageCustom(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        customizeStore.getState().update(reqParams);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getPageCustom().then((setting) => {\n      if (setting) {\n        const formMeta = { ...formData };\n        formMeta.custom_css.value = setting.custom_css;\n        formMeta.custom_head.value = setting.custom_head;\n        formMeta.custom_header.value = setting.custom_header;\n        formMeta.custom_sidebar.value = setting.custom_sidebar;\n        formMeta.custom_footer.value = setting.custom_footer;\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('customize', { keyPrefix: 'nav_menus' })}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Card, Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nconst AnswerLinks = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });\n\n  return (\n    <Card className=\"mb-4\">\n      <Card.Body>\n        <h6 className=\"mb-3\">{t('links')}</h6>\n        <Row>\n          <Col xs={6}>\n            <a\n              href=\"https://answer.apache.org/docs\"\n              target=\"_blank\"\n              rel=\"noreferrer\">\n              {t('documents')}\n            </a>\n          </Col>\n          <Col xs={6}>\n            <a\n              href=\"https://answer.apache.org/plugins\"\n              target=\"_blank\"\n              rel=\"noreferrer\">\n              {t('plugins')}\n            </a>\n          </Col>\n          <Col xs={6}>\n            <a\n              href=\"https://answer.apache.org/community/support\"\n              target=\"_blank\"\n              rel=\"noreferrer\">\n              {t('support')}\n            </a>\n          </Col>\n          <Col xs={6}>\n            <a href=\"https://meta.answer.dev\" target=\"_blank\" rel=\"noreferrer\">\n              {t('forum')}\n            </a>\n          </Col>\n          <Col xs={6}>\n            <a\n              href=\"https://answer.apache.org/blog\"\n              target=\"_blank\"\n              rel=\"noreferrer\">\n              {t('blog')}\n            </a>\n          </Col>\n          <Col xs={6}>\n            <a\n              href=\"https://github.com/apache/answer\"\n              target=\"_blank\"\n              rel=\"noreferrer\">\n              {t('github')}\n            </a>\n          </Col>\n        </Row>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default AnswerLinks;\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport type * as Type from '@/common/interface';\nimport { siteSecurityStore } from '@/stores';\n\nconst { gt, gte } = require('semver');\n\ninterface IProps {\n  data: Type.AdminDashboard['info'];\n}\n\nconst HealthStatus: FC<IProps> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });\n  const { version, remote_version } = data.version_info || {};\n  const { check_update } = siteSecurityStore.getState();\n  let isLatest = false;\n  let hasNewerVersion = false;\n  const downloadUrl = `https://answer.apache.org/download?from_version=${version}`;\n  if (version && remote_version) {\n    isLatest = gte(version, remote_version);\n    hasNewerVersion = gt(remote_version, version);\n  }\n  return (\n    <Card className=\"mb-4\">\n      <Card.Body>\n        <h6 className=\"mb-3\">{t('site_health')}</h6>\n        <Row>\n          <Col xs={6} className=\"mb-1 d-flex align-items-center\">\n            <span className=\"text-secondary me-1\">{t('version')}</span>\n            <strong>{version}</strong>\n            {isLatest && (\n              <a\n                className=\"ms-1 badge rounded-pill text-bg-success\"\n                target=\"_blank\"\n                href={downloadUrl}\n                rel=\"noreferrer\">\n                {t('latest')}\n              </a>\n            )}\n            {!isLatest && hasNewerVersion && (\n              <a\n                className=\"ms-1 badge rounded-pill text-bg-warning\"\n                target=\"_blank\"\n                href={downloadUrl}\n                rel=\"noreferrer\">\n                {t('update_to')} {remote_version}\n              </a>\n            )}\n            {!isLatest && !remote_version && check_update && (\n              <a\n                className=\"ms-1 badge rounded-pill text-bg-danger\"\n                target=\"_blank\"\n                href={downloadUrl}\n                rel=\"noreferrer\">\n                {t('check_failed')}\n              </a>\n            )}\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('run_mode')}</span>\n            <strong>{data.login_required ? t('private') : t('public')}</strong>\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('upload_folder')}</span>\n            <strong>\n              {data.uploading_files ? t('writable') : t('not_writable')}\n            </strong>\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('https')}</span>\n            <strong>{data.https ? t('yes') : t('no')}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('timezone')}</span>\n            <strong>\n              {(data.time_zone.split('/')?.[1] ?? data.time_zone).replaceAll(\n                '_',\n                ' ',\n              )}\n            </strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('smtp')}</span>\n            {data.smtp !== 'not_configured' ? (\n              <strong>{t(data.smtp)}</strong>\n            ) : (\n              <Link to=\"/admin/smtp\" className=\"ms-2\">\n                {t('config')}\n              </Link>\n            )}\n          </Col>\n        </Row>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default HealthStatus;\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport type * as Type from '@/common/interface';\n\ninterface IProps {\n  data: Type.AdminDashboard['info'];\n}\nconst Statistics: FC<IProps> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });\n\n  return (\n    <Card className=\"mb-4\">\n      <Card.Body>\n        <h6 className=\"mb-3\">{t('site_statistics')}</h6>\n        <Row>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('questions')}</span>\n            <strong>{data.question_count}</strong>\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('resolved')}</span>\n            <strong>{data.resolved_count}</strong>\n            {data.resolved_count > 0 ? (\n              <span className=\"text-secondary m-1\">\n                ({data.resolved_rate}%)\n              </span>\n            ) : (\n              ''\n            )}\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('unanswered')}</span>\n            <strong>{data.unanswered_count}</strong>\n            {data.unanswered_count > 0 ? (\n              <span className=\"text-secondary m-1\">\n                ({data.unanswered_rate}%)\n              </span>\n            ) : (\n              ''\n            )}\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('answers')}</span>\n            <strong>{data.answer_count}</strong>\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('comments')}</span>\n            <strong>{data.comment_count}</strong>\n          </Col>\n          <Col xs={6} className=\"mb-1\">\n            <span className=\"text-secondary me-1\">{t('votes')}</span>\n            <strong>{data.vote_count}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('users')}</span>\n            <strong>{data.user_count}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('reviews')}</span>\n            <strong>\n              <Link to=\"/review\" className=\"ms-2\">\n                {data.report_count}\n              </Link>\n            </strong>\n          </Col>\n        </Row>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default Statistics;\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { formatUptime } from '@/utils';\n\ninterface IProps {\n  data: Type.AdminDashboard['info'];\n}\nconst SystemInfo: FC<IProps> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });\n\n  return (\n    <Card className=\"mb-4\">\n      <Card.Body>\n        <h6 className=\"mb-3\">{t('system_info')}</h6>\n        <Row>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('go_version')}</span>\n            <strong>{data.go_version}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('database')}</span>\n            <strong>{data.database_version}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('storage_used')}</span>\n            <strong>{data.occupying_storage_space}</strong>\n          </Col>\n          <Col xs={6}>\n            <span className=\"text-secondary me-1\">{t('database_size')}</span>\n            <strong>{data.database_size}</strong>\n          </Col>\n          {data.app_start_time ? (\n            <Col xs={6}>\n              <span className=\"text-secondary me-1\">{t('uptime')}</span>\n              <strong>{formatUptime(data.app_start_time)}</strong>\n            </Col>\n          ) : null}\n        </Row>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default SystemInfo;\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport SystemInfo from './SystemInfo';\nimport Statistics from './Statistics';\nimport AnswerLinks from './AnswerLinks';\nimport HealthStatus from './HealthStatus';\n\nexport { SystemInfo, Statistics, AnswerLinks, HealthStatus };\n"
  },
  {
    "path": "ui/src/pages/Admin/Dashboard/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { useDashBoard } from '@/services';\n\nimport {\n  AnswerLinks,\n  HealthStatus,\n  Statistics,\n  SystemInfo,\n} from './components';\n\nconst Dashboard: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });\n  const { data } = useDashBoard();\n\n  if (!data) {\n    return null;\n  }\n\n  return (\n    <>\n      <h3 className=\"text-capitalize\">{t('title')}</h3>\n      <p className=\"mt-4\">{t('welcome')}</p>\n      <Row>\n        <Col lg={6}>\n          <Statistics data={data.info} />\n        </Col>\n        <Col lg={6}>\n          <HealthStatus data={data.info} />\n        </Col>\n        <Col lg={6}>\n          <SystemInfo data={data.info} />\n        </Col>\n        <Col lg={6}>\n          <AnswerLinks />\n        </Col>\n      </Row>\n    </>\n  );\n};\nexport default Dashboard;\n"
  },
  {
    "path": "ui/src/pages/Admin/Files/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Form, Button } from 'react-bootstrap';\n\nimport type * as Type from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { getAdminFilesSetting, updateAdminFilesSetting } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { writeSettingStore } from '@/stores';\n\nconst initFormData = {\n  max_image_size: {\n    value: 0,\n    errorMsg: '',\n    isInvalid: false,\n  },\n  max_attachment_size: {\n    value: 0,\n    errorMsg: '',\n    isInvalid: false,\n  },\n  max_image_megapixel: {\n    value: 0,\n    errorMsg: '',\n    isInvalid: false,\n  },\n  authorized_image_extensions: {\n    value: '',\n    errorMsg: '',\n    isInvalid: false,\n  },\n  authorized_attachment_extensions: {\n    value: '',\n    errorMsg: '',\n    isInvalid: false,\n  },\n};\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.write',\n  });\n  const Toast = useToast();\n\n  const [formData, setFormData] = useState(initFormData);\n\n  const handleValueChange = (value) => {\n    setFormData({\n      ...formData,\n      ...value,\n    });\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: Type.AdminSettingsWrite = {\n      max_image_size: Number(formData.max_image_size.value),\n      max_attachment_size: Number(formData.max_attachment_size.value),\n      max_image_megapixel: Number(formData.max_image_megapixel.value),\n      authorized_image_extensions:\n        formData.authorized_image_extensions.value?.length > 0\n          ? formData.authorized_image_extensions.value\n              .split(',')\n              ?.map((item) => item.trim().toLowerCase())\n          : [],\n      authorized_attachment_extensions:\n        formData.authorized_attachment_extensions.value?.length > 0\n          ? formData.authorized_attachment_extensions.value\n              .split(',')\n              ?.map((item) => item.trim().toLowerCase())\n          : [],\n    };\n    updateAdminFilesSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        writeSettingStore.getState().update({ ...reqParams });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const initData = () => {\n    getAdminFilesSetting().then((res) => {\n      formData.max_image_size.value = res.max_image_size;\n      formData.max_attachment_size.value = res.max_attachment_size;\n      formData.max_image_megapixel.value = res.max_image_megapixel;\n      formData.authorized_image_extensions.value =\n        res.authorized_image_extensions?.join(', ').toLowerCase();\n      formData.authorized_attachment_extensions.value =\n        res.authorized_attachment_extensions?.join(', ').toLowerCase();\n      setFormData({ ...formData });\n    });\n  };\n\n  useEffect(() => {\n    initData();\n  }, []);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <Form noValidate onSubmit={onSubmit}>\n          <Form.Group className=\"mb-3\" controlId=\"max_image_size\">\n            <Form.Label>{t('image_size.label')}</Form.Label>\n            <Form.Control\n              type=\"number\"\n              inputMode=\"numeric\"\n              min={0}\n              value={formData.max_image_size.value}\n              isInvalid={formData.max_image_size.isInvalid}\n              onChange={(evt) => {\n                handleValueChange({\n                  max_image_size: {\n                    value: evt.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Text>{t('image_size.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.max_image_size.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"max_attachment_size\">\n            <Form.Label>{t('attachment_size.label')}</Form.Label>\n            <Form.Control\n              type=\"number\"\n              inputMode=\"numeric\"\n              min={0}\n              value={formData.max_attachment_size.value}\n              isInvalid={formData.max_attachment_size.isInvalid}\n              onChange={(evt) => {\n                handleValueChange({\n                  max_attachment_size: {\n                    value: evt.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Text>{t('attachment_size.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.max_attachment_size.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"max_image_megapixel\">\n            <Form.Label>{t('image_megapixels.label')}</Form.Label>\n            <Form.Control\n              type=\"number\"\n              inputMode=\"numeric\"\n              min={0}\n              isInvalid={formData.max_image_megapixel.isInvalid}\n              value={formData.max_image_megapixel.value}\n              onChange={(evt) => {\n                handleValueChange({\n                  max_image_megapixel: {\n                    value: evt.target.value,\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Text>{t('image_megapixels.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.max_image_megapixel.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\" controlId=\"authorized_image_extensions\">\n            <Form.Label>{t('image_extensions.label')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              value={formData.authorized_image_extensions.value}\n              isInvalid={formData.authorized_image_extensions.isInvalid}\n              onChange={(evt) => {\n                handleValueChange({\n                  authorized_image_extensions: {\n                    value: evt.target.value.toLowerCase(),\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Text>{t('image_extensions.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.authorized_image_extensions.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group\n            className=\"mb-3\"\n            controlId=\"authorized_attachment_extensions\">\n            <Form.Label>{t('attachment_extensions.label')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              value={formData.authorized_attachment_extensions.value}\n              isInvalid={formData.authorized_attachment_extensions.isInvalid}\n              onChange={(evt) => {\n                handleValueChange({\n                  authorized_attachment_extensions: {\n                    value: evt.target.value.toLowerCase(),\n                    errorMsg: '',\n                    isInvalid: false,\n                  },\n                });\n              }}\n            />\n            <Form.Text>{t('attachment_extensions.text')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.authorized_attachment_extensions.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group className=\"mb-3\">\n            <Button type=\"submit\">{t('save', { keyPrefix: 'btns' })}</Button>\n          </Form.Group>\n        </Form>\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/General/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport type * as Type from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { siteInfoStore } from '@/stores';\nimport { useGeneralSetting, updateGeneralSetting } from '@/services';\nimport Pattern from '@/common/pattern';\nimport { REACT_BASE_PATH } from '@/router/alias';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst General: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.general',\n  });\n  const Toast = useToast();\n  const updateSiteInfo = siteInfoStore((state) => state.update);\n\n  const { data: setting } = useGeneralSetting();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    required: ['name', 'site_url', 'contact_email'],\n    properties: {\n      name: {\n        type: 'string',\n        title: t('name.label'),\n        description: t('name.text'),\n      },\n      site_url: {\n        type: 'string',\n        title: t('site_url.label'),\n        description: t('site_url.text'),\n      },\n      short_description: {\n        type: 'string',\n        title: `${t('short_desc.label')} ${t('optional', {\n          keyPrefix: 'form',\n        })}`,\n        description: t('short_desc.text'),\n      },\n      description: {\n        type: 'string',\n        title: `${t('desc.label')} ${t('optional', {\n          keyPrefix: 'form',\n        })}`,\n        description: t('desc.text'),\n      },\n      contact_email: {\n        type: 'string',\n        title: t('contact_email.label'),\n        description: t('contact_email.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    site_url: {\n      'ui:options': {\n        inputType: 'url',\n        validator: (value) => {\n          let url: URL | undefined;\n          try {\n            url = new URL(value);\n          } catch (ex) {\n            return t('site_url.validate');\n          }\n          if (\n            !url ||\n            !/^https?:$/.test(url.protocol) ||\n            (REACT_BASE_PATH && url.pathname !== REACT_BASE_PATH) ||\n            (!REACT_BASE_PATH && url.pathname !== '/') ||\n            url.search !== '' ||\n            url.hash !== ''\n          ) {\n            return t('site_url.validate');\n          }\n\n          return true;\n        },\n      },\n    },\n    contact_email: {\n      'ui:options': {\n        inputType: 'email',\n        validator: (value) => {\n          if (!Pattern.email.test(value)) {\n            return t('contact_email.validate');\n          }\n          return true;\n        },\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    const reqParams: Type.AdminSettingsGeneral = {\n      name: formData.name.value,\n      description: formData.description.value,\n      short_description: formData.short_description.value,\n      site_url: formData.site_url.value,\n      contact_email: formData.contact_email.value,\n    };\n\n    updateGeneralSetting(reqParams)\n      .then((res) => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        if (res.name) {\n          formData.name.value = res.name;\n          formData.description.value = res.description;\n          formData.short_description.value = res.short_description;\n          formData.site_url.value = res.site_url;\n          formData.contact_email.value = res.contact_email;\n        }\n\n        setFormData({ ...formData });\n        updateSiteInfo(res);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    if (!setting) {\n      return;\n    }\n    const formMeta: Type.FormDataType = {};\n    Object.keys(formData).forEach((k) => {\n      formMeta[k] = { ...formData[k], value: setting[k] };\n    });\n    setFormData({ ...formData, ...formMeta });\n  }, [setting]);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default General;\n"
  },
  {
    "path": "ui/src/pages/Admin/Interface/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, FormEvent, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useToast } from '@/hooks';\nimport {\n  LangsType,\n  FormDataType,\n  AdminSettingsInterface,\n} from '@/common/interface';\nimport { interfaceStore, loggedUserInfoStore } from '@/stores';\nimport { JSONSchema, SchemaForm, UISchema } from '@/components';\nimport { DEFAULT_TIMEZONE } from '@/common/constants';\nimport {\n  updateInterfaceSetting,\n  useInterfaceSetting,\n  getLoggedUserInfo,\n} from '@/services';\nimport {\n  setupAppLanguage,\n  loadLanguageOptions,\n  setupAppTimeZone,\n} from '@/utils/localize';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Interface: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.interface',\n  });\n  const storeInterface = interfaceStore.getState().interface;\n  const Toast = useToast();\n  const [langs, setLangs] = useState<LangsType[]>();\n  const { data: setting } = useInterfaceSetting();\n\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      language: {\n        type: 'string',\n        title: t('language.label'),\n        description: t('language.text'),\n        enum: langs?.map((lang) => lang.value),\n        enumNames: langs?.map((lang) => lang.label),\n        default:\n          setting?.language || storeInterface.language || langs?.[0]?.value,\n      },\n      time_zone: {\n        type: 'string',\n        title: t('time_zone.label'),\n        description: t('time_zone.text'),\n        default: setting?.time_zone || DEFAULT_TIMEZONE,\n      },\n    },\n  };\n\n  const [formData, setFormData] = useState<FormDataType>({\n    language: {\n      value: setting?.language || storeInterface.language || langs?.[0]?.value,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    time_zone: {\n      value: setting?.time_zone || DEFAULT_TIMEZONE,\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const uiSchema: UISchema = {\n    language: {\n      'ui:widget': 'select',\n    },\n    time_zone: {\n      'ui:widget': 'timezone',\n    },\n  };\n  const getLangs = async () => {\n    const res: LangsType[] = await loadLanguageOptions(true);\n    setLangs(res);\n  };\n\n  const checkValidated = (): boolean => {\n    let ret = true;\n    const { language } = formData;\n    const formCheckData = { ...formData };\n    if (!language.value) {\n      ret = false;\n      formCheckData.language = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('language.msg'),\n      };\n    }\n    setFormData({\n      ...formCheckData,\n    });\n    return ret;\n  };\n  const onSubmit = (evt: FormEvent) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    if (checkValidated() === false) {\n      return;\n    }\n    const reqParams: AdminSettingsInterface = {\n      language: formData.language.value,\n      time_zone: formData.time_zone.value,\n    };\n\n    updateInterfaceSetting(reqParams)\n      .then(() => {\n        interfaceStore.getState().update(reqParams);\n        setupAppLanguage();\n        setupAppTimeZone();\n        getLoggedUserInfo().then((info) => {\n          loggedUserInfoStore.getState().update(info);\n        });\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    if (setting) {\n      const formMeta = { ...formData };\n      if (setting.language) {\n        formMeta.language.value = setting.language;\n      } else {\n        formMeta.language.value = storeInterface.language || langs?.[0]?.value;\n      }\n      if (setting.time_zone) {\n        formMeta.time_zone.value = setting.time_zone;\n      }\n      setFormData({ ...formData, ...formMeta });\n    }\n  }, [setting, langs]);\n  useEffect(() => {\n    getLangs();\n  }, []);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onSubmit={onSubmit}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Interface;\n"
  },
  {
    "path": "ui/src/pages/Admin/Login/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { getLoginSetting, putLoginSetting } from '@/services';\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport { useToast } from '@/hooks';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { loginSettingStore } from '@/stores';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.login',\n  });\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      allow_new_registrations: {\n        type: 'boolean',\n        title: t('membership.title'),\n        description: t('membership.text'),\n        default: true,\n      },\n      allow_email_registrations: {\n        type: 'boolean',\n        title: t('email_registration.title'),\n        description: t('email_registration.text'),\n        default: true,\n      },\n      allow_password_login: {\n        type: 'boolean',\n        title: t('password_login.title'),\n        description: t('password_login.text'),\n        default: true,\n      },\n      allow_email_domains: {\n        type: 'string',\n        title: t('allowed_email_domains.title'),\n        description: t('allowed_email_domains.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    allow_new_registrations: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('membership.label'),\n      },\n    },\n    allow_email_registrations: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('email_registration.label'),\n      },\n    },\n    allow_password_login: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('password_login.label'),\n      },\n    },\n    allow_email_domains: {\n      'ui:widget': 'textarea',\n    },\n  };\n  const [formData, setFormData] = useState(initFormData(schema));\n  const { update: updateLoginSetting } = loginSettingStore((_) => _);\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const allowedEmailDomains: string[] = [];\n    if (formData.allow_email_domains.value) {\n      const domainList = formData.allow_email_domains.value.split('\\n');\n      domainList.forEach((li) => {\n        li = li.trim();\n        if (li) {\n          allowedEmailDomains.push(li);\n        }\n      });\n    }\n    const reqParams: Type.AdminSettingsLogin = {\n      allow_new_registrations: formData.allow_new_registrations.value,\n      allow_email_registrations: formData.allow_email_registrations.value,\n      allow_email_domains: allowedEmailDomains,\n      allow_password_login: formData.allow_password_login.value,\n    };\n\n    putLoginSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        updateLoginSetting(reqParams);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getLoginSetting().then((setting) => {\n      if (setting) {\n        const formMeta = { ...formData };\n        formMeta.allow_new_registrations.value =\n          setting.allow_new_registrations;\n        formMeta.allow_email_registrations.value =\n          setting.allow_email_registrations;\n        formMeta.allow_email_domains.value = '';\n        if (Array.isArray(setting.allow_email_domains)) {\n          formMeta.allow_email_domains.value =\n            setting.allow_email_domains.join('\\n');\n        }\n        formMeta.allow_password_login.value = setting.allow_password_login;\n        setFormData({ ...formMeta });\n      }\n    });\n  }, []);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Mcp/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FormEvent, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Form, Button } from 'react-bootstrap';\n\nimport { useToast } from '@/hooks';\nimport { getMcpConfig, saveMcpConfig } from '@/services';\n\nconst Mcp = () => {\n  const toast = useToast();\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.mcp',\n  });\n  const [formData, setFormData] = useState({\n    enabled: true,\n    type: '',\n    url: '',\n    http_header: '',\n  });\n  const [isLoading, setIsLoading] = useState(false);\n\n  const handleOnChange = (form) => {\n    setFormData({ ...formData, ...form });\n  };\n  const onSubmit = (evt: FormEvent) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    saveMcpConfig({ enabled: formData.enabled }).then(() => {\n      toast.onShow({\n        msg: t('update', { keyPrefix: 'toast' }),\n        variant: 'success',\n      });\n    });\n  };\n\n  useEffect(() => {\n    getMcpConfig()\n      .then((resp) => {\n        setIsLoading(false);\n        setFormData(resp);\n      })\n      .catch(() => {\n        setIsLoading(false);\n      });\n  }, []);\n  if (isLoading) {\n    return null;\n  }\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('mcp', { keyPrefix: 'nav_menus' })}</h3>\n      <div className=\"max-w-748\">\n        <Form onSubmit={onSubmit}>\n          <Form.Group className=\"mb-3\" controlId=\"mcp_server\">\n            <Form.Label>{t('mcp_server.label')}</Form.Label>\n            <Form.Check\n              type=\"switch\"\n              label={t('mcp_server.switch')}\n              checked={formData.enabled}\n              onChange={(e) => handleOnChange({ enabled: e.target.checked })}\n            />\n          </Form.Group>\n          {formData.enabled && (\n            <>\n              <Form.Group className=\"mb-3\" controlId=\"type\">\n                <Form.Label>{t('type.label')}</Form.Label>\n                <Form.Control type=\"text\" disabled value={formData.type} />\n              </Form.Group>\n              <Form.Group className=\"mb-3\" controlId=\"url\">\n                <Form.Label>{t('url.label')}</Form.Label>\n                <Form.Control type=\"text\" disabled value={formData.url} />\n              </Form.Group>\n              <Form.Group className=\"mb-3\">\n                <Form.Label>{t('http_header.label')}</Form.Label>\n                <Form.Control\n                  type=\"text\"\n                  disabled\n                  value={formData.http_header}\n                />\n                <Form.Text className=\"text-muted\">\n                  {t('http_header.text')}\n                </Form.Text>\n              </Form.Group>\n            </>\n          )}\n          <Button variant=\"primary\" type=\"submit\">\n            {t('save', { keyPrefix: 'btns' })}\n          </Button>\n        </Form>\n      </div>\n    </>\n  );\n};\n\nexport default Mcp;\n"
  },
  {
    "path": "ui/src/pages/Admin/Plugins/Config/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useParams } from 'react-router-dom';\n\nimport { useToast } from '@/hooks';\nimport type * as Types from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema } from '@/components';\nimport { useQueryPluginConfig, updatePluginConfig } from '@/services';\nimport { InputOptions, FormKit, initFormData } from '@/components/SchemaForm';\n\nconst Config = () => {\n  const { t } = useTranslation('translation');\n  const { slug_name } = useParams<{ slug_name: string }>();\n  const { data, mutate: refreshPluginConfig } = useQueryPluginConfig({\n    plugin_slug_name: slug_name,\n  });\n  const Toast = useToast();\n  const [schema, setSchema] = useState<JSONSchema | null>(null);\n  const [uiSchema, setUISchema] = useState<UISchema>();\n  const required: string[] = [];\n\n  const [formData, setFormData] = useState<Types.FormDataType | null>(null);\n\n  useEffect(() => {\n    if (!data) {\n      return;\n    }\n    const properties: JSONSchema['properties'] = {};\n    const uiConf: UISchema = {};\n    data.config_fields?.forEach((item) => {\n      properties[item.name] = {\n        type: 'string',\n        title: item.title,\n        description: item.description,\n        default: item.value,\n      };\n\n      if (item.options instanceof Array) {\n        properties[item.name].enum = item.options.map((option) => option.value);\n        properties[item.name].enumNames = item.options.map(\n          (option) => option.label,\n        );\n      }\n      uiConf[item.name] = {};\n      uiConf[item.name]['ui:widget'] = item.type;\n      if (item.ui_options) {\n        if ((item.ui_options as InputOptions & { input_type })?.input_type) {\n          (item.ui_options as InputOptions).inputType = (\n            item.ui_options as InputOptions & { input_type }\n          ).input_type;\n        }\n        uiConf[item.name]['ui:options'] = item.ui_options;\n      }\n      if (item.required) {\n        required.push(item.name);\n      }\n    });\n    const result = {\n      title: data?.name || '',\n      required,\n      properties,\n    };\n    setFormData(initFormData(result));\n    setSchema(result);\n    setUISchema(uiConf);\n  }, [data?.config_fields]);\n\n  const onSubmit = (evt) => {\n    if (!formData) {\n      return;\n    }\n    evt.preventDefault();\n    evt.stopPropagation();\n    const config_fields = {};\n    Object.keys(formData).forEach((key) => {\n      config_fields[key] = formData[key].value;\n    });\n    const params = {\n      plugin_slug_name: slug_name,\n      config_fields,\n    };\n    updatePluginConfig(params).then(() => {\n      Toast.onShow({\n        msg: t('update', { keyPrefix: 'toast' }),\n        variant: 'success',\n      });\n    });\n  };\n  const refreshConfig: FormKit['refreshConfig'] = async () => {\n    refreshPluginConfig();\n  };\n  const handleOnChange = (form) => {\n    setFormData(form);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{data?.name}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          refreshConfig={refreshConfig}\n          formData={formData}\n          onSubmit={onSubmit}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Config;\n"
  },
  {
    "path": "ui/src/pages/Admin/Plugins/Installed/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Table, Dropdown, Stack } from 'react-bootstrap';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\nimport { useTranslation, Trans } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { Empty, QueryGroup, Icon } from '@/components';\nimport * as Type from '@/common/interface';\nimport { useQueryPlugins, updatePluginStatus } from '@/services';\nimport PluginKit from '@/utils/pluginKit';\n\nconst InstalledPluginsFilterKeys: Type.InstalledPluginsFilterBy[] = [\n  'all',\n  'active',\n  'inactive',\n];\n\nconst bgMap = {\n  active: 'text-bg-success',\n  inactive: 'text-bg-secondary',\n};\n\nconst Users: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.installed_plugins',\n  });\n  const navigate = useNavigate();\n  const [urlSearchParams] = useSearchParams();\n  const curFilter =\n    urlSearchParams.get('filter') || InstalledPluginsFilterKeys[0];\n  const {\n    data,\n    isLoading,\n    mutate: updatePlugins,\n  } = useQueryPlugins({\n    status: curFilter === 'all' ? undefined : curFilter,\n  });\n  const emitPluginChange = (type) => {\n    window.postMessage({\n      msgType: type,\n    });\n  };\n  const handleStatus = (plugin) => {\n    updatePluginStatus({\n      enabled: !plugin.enabled,\n      plugin_slug_name: plugin.slug_name,\n    }).then(() => {\n      updatePlugins();\n      PluginKit.refresh();\n      if (plugin.have_config) {\n        emitPluginChange('refreshConfigurablePlugins');\n      }\n    });\n  };\n  const handleSettings = (plugin) => {\n    const url = `/admin/${plugin.slug_name}`;\n    navigate(url);\n  };\n\n  return (\n    <>\n      <h3>{t('title')}</h3>\n      <div className=\"mb-4\">\n        <Trans i18nKey=\"admin.installed_plugins.plugin_link\">\n          Plugins extend and expand the functionality. You may find plugins in\n          the\n          <a\n            href=\"https://github.com/apache/answer-plugins\"\n            target=\"_blank\"\n            rel=\"noreferrer\">\n            Plugin Repository\n          </a>\n          .\n        </Trans>\n      </div>\n      <div className=\"d-flex justify-content-between align-items-center mb-3\">\n        <Stack direction=\"horizontal\" gap={3}>\n          <QueryGroup\n            data={InstalledPluginsFilterKeys}\n            currentSort={curFilter}\n            sortKey=\"filter\"\n            i18nKeyPrefix=\"admin.installed_plugins.filter\"\n          />\n        </Stack>\n      </div>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th className=\"min-w-15\">{t('name')}</th>\n            <th style={{ width: '17%' }}>{t('version')}</th>\n            <th style={{ width: '11%' }}>{t('status')}</th>\n            {curFilter !== 'deleted' ? (\n              <th style={{ width: '11%' }} className=\"text-end\">\n                {t('action')}\n              </th>\n            ) : null}\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {data?.map((plugin) => {\n            return (\n              <tr key={plugin.slug_name}>\n                <td>\n                  <div>\n                    {plugin.link ? (\n                      <a href={plugin.link} target=\"_blank\" rel=\"noreferrer\">\n                        {plugin.name}\n                      </a>\n                    ) : (\n                      plugin.name\n                    )}\n                  </div>\n                  <div className=\"small\">{plugin.description}</div>\n                </td>\n                <td className=\"text-break\">{plugin.version}</td>\n                <td>\n                  <span\n                    className={classNames(\n                      'badge',\n                      bgMap[plugin.enabled ? 'active' : 'inactive'],\n                    )}>\n                    {t(`filter.${plugin.enabled ? 'active' : 'inactive'}`)}\n                  </span>\n                </td>\n                {curFilter !== 'deleted' ? (\n                  <td className=\"text-end\">\n                    <Dropdown>\n                      <Dropdown.Toggle variant=\"link\" className=\"no-toggle\">\n                        <Icon name=\"three-dots-vertical\" title={t('action')} />\n                      </Dropdown.Toggle>\n                      <Dropdown.Menu>\n                        {plugin.enabled ? (\n                          <Dropdown.Item onClick={() => handleStatus(plugin)}>\n                            {t('deactivate')}\n                          </Dropdown.Item>\n                        ) : (\n                          <Dropdown.Item onClick={() => handleStatus(plugin)}>\n                            {t('activate')}\n                          </Dropdown.Item>\n                        )}\n                        {plugin.enabled && plugin.have_config && (\n                          <Dropdown.Item onClick={() => handleSettings(plugin)}>\n                            {t('settings')}\n                          </Dropdown.Item>\n                        )}\n                      </Dropdown.Menu>\n                    </Dropdown>\n                  </td>\n                ) : null}\n              </tr>\n            );\n          })}\n        </tbody>\n      </Table>\n      {Number(data?.length) <= 0 && !isLoading && <Empty />}\n    </>\n  );\n};\n\nexport default Users;\n"
  },
  {
    "path": "ui/src/pages/Admin/Policies/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { marked } from 'marked';\n\nimport type * as Type from '@/common/interface';\nimport {\n  SchemaForm,\n  JSONSchema,\n  initFormData,\n  UISchema,\n  TabNav,\n} from '@/components';\nimport { useToast } from '@/hooks';\nimport { getPoliciesSetting, putPoliciesSetting } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { ADMIN_RULES_NAV_MENUS } from '@/common/constants';\n\nconst Legal: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.legal',\n  });\n  const Toast = useToast();\n\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    required: ['terms_of_service', 'privacy_policy'],\n    properties: {\n      terms_of_service: {\n        type: 'string',\n        title: t('terms_of_service.label'),\n        description: t('terms_of_service.text'),\n      },\n      privacy_policy: {\n        type: 'string',\n        title: t('privacy_policy.label'),\n        description: t('privacy_policy.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    terms_of_service: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n      },\n    },\n    privacy_policy: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n      },\n    },\n  };\n  const [formData, setFormData] = useState(initFormData(schema));\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: Type.AdminSettingsLegal = {\n      terms_of_service_original_text: formData.terms_of_service.value,\n      terms_of_service_parsed_text: marked.parse(\n        formData.terms_of_service.value,\n      ),\n      privacy_policy_original_text: formData.privacy_policy.value,\n      privacy_policy_parsed_text: marked.parse(formData.privacy_policy.value),\n    };\n\n    putPoliciesSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getPoliciesSetting().then((setting) => {\n      if (setting) {\n        const formMeta = { ...formData };\n        formMeta.terms_of_service.value =\n          setting.terms_of_service_original_text;\n        formMeta.privacy_policy.value = setting.privacy_policy_original_text;\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('rules', { keyPrefix: 'nav_menus' })}</h3>\n      <TabNav menus={ADMIN_RULES_NAV_MENUS} />\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Legal;\n"
  },
  {
    "path": "ui/src/pages/Admin/Privileges/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, FormEvent, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useToast } from '@/hooks';\nimport { FormDataType } from '@/common/interface';\nimport {\n  JSONSchema,\n  SchemaForm,\n  UISchema,\n  initFormData,\n  TabNav,\n} from '@/components';\nimport {\n  getPrivilegeSetting,\n  putPrivilegeSetting,\n  AdminSettingsPrivilege,\n  AdminSettingsPrivilegeReq,\n} from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport {\n  ADMIN_PRIVILEGE_CUSTOM_LEVEL,\n  ADMIN_RULES_NAV_MENUS,\n} from '@/common/constants';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.privilege',\n  });\n  const Toast = useToast();\n  const [privilege, setPrivilege] = useState<AdminSettingsPrivilege>();\n  const [schema, setSchema] = useState<JSONSchema>({\n    title: t('title'),\n    properties: {},\n  });\n  const [uiSchema, setUiSchema] = useState<UISchema>({\n    level: {\n      'ui:widget': 'select',\n    },\n  });\n  const [formData, setFormData] = useState<FormDataType>(initFormData(schema));\n\n  const setFormConfig = (state: FormDataType) => {\n    const selectedLevel = Number(state.level.value);\n    const levelOptions = privilege?.options;\n    const curLevel = levelOptions?.find((li) => {\n      return li.level === selectedLevel;\n    });\n    if (!levelOptions || !curLevel) {\n      return;\n    }\n    const uiState = {\n      level: uiSchema.level,\n    };\n    const props: JSONSchema['properties'] = {\n      level: {\n        type: 'number',\n        title: t('level.label'),\n        description: t('level.text'),\n        enum: levelOptions.map((_) => _.level),\n        enumNames: levelOptions.map((_) => _.level_desc),\n        default: selectedLevel,\n      },\n    };\n    curLevel.privileges.forEach((li) => {\n      props[li.key] = {\n        type: 'number',\n        title: li.label,\n        default: li.value,\n      };\n      uiState[li.key] = {\n        'ui:options': {\n          readOnly: curLevel.level !== ADMIN_PRIVILEGE_CUSTOM_LEVEL,\n          validator: (value: string) => {\n            const val = Number(value);\n            if (Number.isNaN(val)) {\n              return t('msg.should_be_number');\n            }\n            if (val < 1) {\n              return t('msg.number_larger_1');\n            }\n            return true;\n          },\n          inputType: 'number',\n          inputMode: 'numeric',\n        },\n      };\n    });\n    const schemaState = {\n      ...schema,\n      properties: props,\n    };\n    const formState = initFormData(schemaState);\n    curLevel.privileges.forEach((li) => {\n      formState[li.key] = {\n        value: li.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    });\n    setSchema(schemaState);\n    setUiSchema(uiState);\n    setFormData(formState);\n  };\n\n  const onSubmit = (evt: FormEvent) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: AdminSettingsPrivilegeReq = {\n      level: Number(formData.level.value),\n      custom_privileges: [],\n    };\n\n    if (reqParams.level === ADMIN_PRIVILEGE_CUSTOM_LEVEL) {\n      // construct custom level request data\n      Object.entries(formData).forEach(([key, value]) => {\n        if (key === 'level') {\n          return;\n        }\n        reqParams.custom_privileges?.push({\n          key,\n          value: Number(value.value),\n        });\n      });\n    }\n\n    putPrivilegeSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        if (reqParams.level === ADMIN_PRIVILEGE_CUSTOM_LEVEL) {\n          getPrivilegeSetting().then((resp) => {\n            setPrivilege(resp);\n          });\n        }\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    if (!privilege) {\n      return;\n    }\n    setFormConfig({\n      level: {\n        value: privilege.selected_level,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  }, [privilege]);\n  useEffect(() => {\n    getPrivilegeSetting().then((resp) => {\n      setPrivilege(resp);\n    });\n  }, []);\n  const handleOnChange = (state) => {\n    // if updated values in Custom form\n    if (\n      state.level.value === ADMIN_PRIVILEGE_CUSTOM_LEVEL &&\n      formData?.level?.value === state.level.value\n    ) {\n      setFormData(state);\n    } else {\n      setFormConfig(state);\n    }\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('rules', { keyPrefix: 'nav_menus' })}</h3>\n      <TabNav menus={ADMIN_RULES_NAV_MENUS} />\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onSubmit={onSubmit}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/QaSettings/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n  SchemaForm,\n  JSONSchema,\n  UISchema,\n  initFormData,\n  TabNav,\n} from '@/components';\nimport { ADMIN_QA_NAV_MENUS } from '@/common/constants';\nimport * as Type from '@/common/interface';\nimport { writeSettingStore } from '@/stores';\nimport {\n  getQuestionSetting,\n  updateQuestionSetting,\n} from '@/services/admin/question';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { useToast } from '@/hooks';\n\nconst QaSettings = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.write',\n  });\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      min_tags: {\n        type: 'number',\n        title: t('min_tags.label'),\n        description: t('min_tags.text'),\n        default: 0,\n      },\n      min_content: {\n        type: 'number',\n        title: t('min_content.label'),\n        description: t('min_content.text'),\n      },\n      restrict_answer: {\n        type: 'boolean',\n        title: t('restrict_answer.label'),\n        description: t('restrict_answer.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    min_tags: {\n      'ui:widget': 'input',\n      'ui:options': {\n        inputType: 'number',\n      },\n    },\n    min_content: {\n      'ui:widget': 'input',\n      'ui:options': {\n        inputType: 'number',\n      },\n    },\n    restrict_answer: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('restrict_answer.label'),\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const handleValueChange = (data: Type.FormDataType) => {\n    setFormData(data);\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    // TODO: submit data\n    const reqParams: Type.AdminQuestionSetting = {\n      min_tags: formData.min_tags.value,\n      min_content: formData.min_content.value,\n      restrict_answer: formData.restrict_answer.value,\n    };\n    updateQuestionSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        writeSettingStore.getState().update({ ...reqParams });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getQuestionSetting().then((res) => {\n      if (res) {\n        const formMeta = { ...formData };\n        formMeta.min_tags.value = res.min_tags;\n        formMeta.min_content.value = res.min_content;\n        formMeta.restrict_answer.value = res.restrict_answer;\n        console.log('res', res, formMeta);\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">\n        {t('page_title', { keyPrefix: 'admin.questions' })}\n      </h3>\n      <TabNav menus={ADMIN_QA_NAV_MENUS} />\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onChange={handleValueChange}\n          onSubmit={onSubmit}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default QaSettings;\n"
  },
  {
    "path": "ui/src/pages/Admin/Questions/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { Icon, Modal } from '@/components';\nimport {\n  changeQuestionStatus,\n  questionOperation,\n  reopenQuestion,\n} from '@/services';\nimport { useReportModal, useToast } from '@/hooks';\nimport { toastStore } from '@/stores';\n\nconst AnswerActions = ({ itemData, refreshList, curFilter, show, pin }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'delete' });\n  const toast = useToast();\n  const closeCallback = () => {\n    toastStore.getState().show({\n      msg: t('post_closed', { keyPrefix: 'messages' }),\n      variant: 'success',\n    });\n    refreshList();\n  };\n  const closeModal = useReportModal(closeCallback);\n\n  const handleAction = (type) => {\n    if (type === 'delete') {\n      Modal.confirm({\n        title: t('title', { keyPrefix: 'delete' }),\n        content:\n          itemData.answer_count > 0\n            ? t('question', { keyPrefix: 'delete' })\n            : t('other', { keyPrefix: 'delete' }),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('delete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          changeQuestionStatus(itemData.id, 'deleted').then(() => {\n            toastStore.getState().show({\n              msg: t('post_deleted', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n\n    if (type === 'undelete') {\n      Modal.confirm({\n        title: t('undelete_title'),\n        content: t('undelete_desc'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('undelete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          changeQuestionStatus(itemData.id, 'available').then(() => {\n            toastStore.getState().show({\n              msg: t('post_cancel_deleted', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n\n    if (type === 'close') {\n      closeModal.onShow({\n        type: 'question',\n        id: itemData.id,\n        action: 'close',\n      });\n    }\n\n    if (type === 'reopen') {\n      Modal.confirm({\n        title: t('title', { keyPrefix: 'question_detail.reopen' }),\n        content: t('content', { keyPrefix: 'question_detail.reopen' }),\n        cancelBtnVariant: 'link',\n        confirmText: t('confirm_btn', { keyPrefix: 'question_detail.reopen' }),\n        onConfirm: () => {\n          reopenQuestion({\n            question_id: itemData.id,\n          }).then(() => {\n            toastStore.getState().show({\n              msg: t('post_reopen', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n\n    if (type === 'list' || type === 'unlist') {\n      const keyPrefix =\n        type === 'list' ? 'question_detail.list' : 'question_detail.unlist';\n      Modal.confirm({\n        title: t('title', { keyPrefix }),\n        content: t('content', { keyPrefix }),\n        cancelBtnVariant: 'link',\n        confirmText: t('confirm_btn', { keyPrefix }),\n        onConfirm: () => {\n          questionOperation({\n            id: itemData.id,\n            operation: type === 'list' ? 'show' : 'hide',\n          }).then(() => {\n            toast.onShow({\n              msg: t(`post_${type}`, { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            refreshList();\n          });\n        },\n      });\n    }\n  };\n\n  if (curFilter === 'pending') {\n    return (\n      <Link\n        to={`/review?type=queued_post&objectId=${itemData.id}`}\n        className=\"btn btn-link p-0\"\n        title={t('review', { keyPrefix: 'header.nav' })}>\n        <Icon name=\"three-dots-vertical\" />\n      </Link>\n    );\n  }\n\n  return (\n    <Dropdown>\n      <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0\">\n        <Icon\n          name=\"three-dots-vertical\"\n          title={t('action', { keyPrefix: 'admin.answers' })}\n        />\n      </Dropdown.Toggle>\n      <Dropdown.Menu align=\"end\">\n        {curFilter === 'normal' && (\n          <Dropdown.Item onClick={() => handleAction('close')}>\n            {t('close', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        )}\n        {curFilter === 'closed' && (\n          <Dropdown.Item onClick={() => handleAction('reopen')}>\n            {t('reopen', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        )}\n        {curFilter !== 'deleted' ? (\n          <Dropdown.Item onClick={() => handleAction('delete')}>\n            {t('delete', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        ) : (\n          <Dropdown.Item onClick={() => handleAction('undelete')}>\n            {t('undelete', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        )}\n        {show === 2 ? (\n          <Dropdown.Item onClick={() => handleAction('list')}>\n            {t('list', { keyPrefix: 'btns' })}\n          </Dropdown.Item>\n        ) : (\n          pin !== 2 && (\n            <Dropdown.Item onClick={() => handleAction('unlist')}>\n              {t('unlist', { keyPrefix: 'btns' })}\n            </Dropdown.Item>\n          )\n        )}\n      </Dropdown.Menu>\n    </Dropdown>\n  );\n};\n\nexport default AnswerActions;\n"
  },
  {
    "path": "ui/src/pages/Admin/Questions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Form, Table, Stack, Button } from 'react-bootstrap';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport {\n  FormatTime,\n  Icon,\n  Pagination,\n  BaseUserCard,\n  Empty,\n  QueryGroup,\n  Modal,\n  TabNav,\n} from '@/components';\nimport { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';\nimport * as Type from '@/common/interface';\nimport { deletePermanently, useQuestionSearch } from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\nimport { toastStore } from '@/stores';\n\nimport Action from './components/Action';\n\nconst questionFilterItems: Type.AdminContentsFilterBy[] = [\n  'normal',\n  'pending',\n  'closed',\n  'deleted',\n];\n\nconst PAGE_SIZE = 20;\nconst Questions: FC = () => {\n  const [urlSearchParams, setUrlSearchParams] = useSearchParams();\n  const curFilter = urlSearchParams.get('status') || questionFilterItems[0];\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const curQuery = urlSearchParams.get('query') || '';\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' });\n\n  const {\n    data: listData,\n    isLoading,\n    mutate: refreshList,\n  } = useQuestionSearch({\n    page_size: PAGE_SIZE,\n    page: curPage,\n    status: curFilter as Type.AdminContentsFilterBy,\n    query: curQuery,\n  });\n  const count = listData?.count || 0;\n\n  const handleDeletePermanently = () => {\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'delete_permanently' }),\n      content: t('content', { keyPrefix: 'delete_permanently' }),\n      cancelBtnVariant: 'link',\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      confirmBtnVariant: 'danger',\n      onConfirm: () => {\n        deletePermanently('questions').then(() => {\n          toastStore.getState().show({\n            msg: t('posts_deleted', { keyPrefix: 'messages' }),\n            variant: 'success',\n          });\n          refreshList();\n        });\n      },\n    });\n  };\n\n  const handleFilter = (e) => {\n    urlSearchParams.set('query', e.target.value);\n    urlSearchParams.delete('page');\n    setUrlSearchParams(urlSearchParams);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <TabNav menus={ADMIN_QA_NAV_MENUS} />\n      <div className=\"d-flex flex-wrap justify-content-between align-items-center\">\n        <Stack direction=\"horizontal\" gap={3} className=\"mb-3\">\n          <QueryGroup\n            data={questionFilterItems}\n            currentSort={curFilter}\n            sortKey=\"status\"\n            i18nKeyPrefix=\"btns\"\n          />\n          {curFilter === 'deleted' && count > 0 ? (\n            <Button\n              variant=\"outline-danger\"\n              size=\"sm\"\n              onClick={() => handleDeletePermanently()}>\n              {t('deleted_permanently', { keyPrefix: 'btns' })}\n            </Button>\n          ) : null}\n        </Stack>\n\n        <Form.Control\n          value={curQuery}\n          size=\"sm\"\n          type=\"search\"\n          placeholder={t('filter.placeholder')}\n          onChange={handleFilter}\n          style={{ width: '12.25rem' }}\n          className=\"mb-3\"\n        />\n      </div>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th className=\"min-w-15\">{t('post')}</th>\n            <th style={{ width: '8%' }}>{t('votes')}</th>\n            <th style={{ width: '8%' }}>{t('answers')}</th>\n            <th style={{ width: '15%' }}>{t('created')}</th>\n            <th style={{ width: '14%' }}>{t('status')}</th>\n            <th style={{ width: '10%' }} className=\"text-end\">\n              {t('action')}\n            </th>\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {listData?.list?.map((li) => {\n            return (\n              <tr key={li.id}>\n                <td>\n                  <Link\n                    to={pathFactory.questionLanding(li.id, li.url_title)}\n                    target=\"_blank\"\n                    className=\"text-break text-wrap\"\n                    rel=\"noreferrer\">\n                    {li.title}\n                  </Link>\n                  {li.accepted_answer_id > 0 && (\n                    <Icon\n                      name=\"check-circle-fill\"\n                      className=\"ms-2 text-success\"\n                    />\n                  )}\n                </td>\n                <td>{li.vote_count}</td>\n                <td>\n                  <Link\n                    to={`/admin/answers?questionId=${li.id}`}\n                    rel=\"noreferrer\">\n                    {li.answer_count}\n                  </Link>\n                </td>\n                <td>\n                  <Stack>\n                    <BaseUserCard\n                      avatarSize=\"20\"\n                      data={li.user_info}\n                      nameMaxWidth=\"130px\"\n                    />\n                    <FormatTime\n                      className=\"small text-secondary\"\n                      time={li.create_time}\n                    />\n                  </Stack>\n                </td>\n                <td>\n                  <span\n                    className={classNames(\n                      'badge',\n                      'me-1',\n                      'mb-1',\n                      ADMIN_LIST_STATUS[curFilter]?.variant,\n                    )}>\n                    {t(ADMIN_LIST_STATUS[curFilter]?.name, {\n                      keyPrefix: 'btns',\n                    })}\n                  </span>\n                  {li.show === 2 && (\n                    <span\n                      className={classNames(\n                        'badge',\n                        ADMIN_LIST_STATUS.unlisted.variant,\n                      )}>\n                      {t(ADMIN_LIST_STATUS.unlisted.name, {\n                        keyPrefix: 'btns',\n                      })}\n                    </span>\n                  )}\n                </td>\n\n                <td className=\"text-end\">\n                  <Action\n                    itemData={{ id: li.id, answer_count: li.answer_count }}\n                    refreshList={refreshList}\n                    curFilter={curFilter}\n                    show={li.show}\n                    pin={li.pin}\n                  />\n                </td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </Table>\n      {Number(count) <= 0 && !isLoading && <Empty />}\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={count}\n          pageSize={PAGE_SIZE}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Questions;\n"
  },
  {
    "path": "ui/src/pages/Admin/Security/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport { siteSecurityStore } from '@/stores';\nimport {\n  getSecuritySetting,\n  putSecuritySetting,\n} from '@/services/admin/settings';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { useToast } from '@/hooks';\n\nconst Security = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.security',\n  });\n  const Toast = useToast();\n  const externalContent = [\n    {\n      value: 'always_display',\n      label: t('external_content_display.always_display', {\n        keyPrefix: 'admin.legal',\n      }),\n    },\n    {\n      value: 'ask_before_display',\n      label: t('external_content_display.ask_before_display', {\n        keyPrefix: 'admin.legal',\n      }),\n    },\n  ];\n\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      login_required: {\n        type: 'boolean',\n        title: t('private.title', { keyPrefix: 'admin.login' }),\n        description: t('private.text', { keyPrefix: 'admin.login' }),\n        default: false,\n      },\n      external_content_display: {\n        type: 'string',\n        title: t('external_content_display.label', {\n          keyPrefix: 'admin.legal',\n        }),\n        description: t('external_content_display.text', {\n          keyPrefix: 'admin.legal',\n        }),\n        enum: externalContent?.map((lang) => lang.value),\n        enumNames: externalContent?.map((lang) => lang.label),\n        default: 0,\n      },\n      check_update: {\n        type: 'boolean',\n        title: t('check_update.label', { keyPrefix: 'admin.general' }),\n        default: true,\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    login_required: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('private.label', { keyPrefix: 'admin.login' }),\n      },\n    },\n    external_content_display: {\n      'ui:widget': 'select',\n      'ui:options': {\n        label: t('external_content_display.label', {\n          keyPrefix: 'admin.legal',\n        }),\n      },\n    },\n    check_update: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('check_update.label', { keyPrefix: 'admin.general' }),\n      },\n    },\n  };\n  const [formData, setFormData] = useState(initFormData(schema));\n\n  const handleValueChange = (data: Type.FormDataType) => {\n    setFormData(data);\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    const reqParams = {\n      login_required: formData.login_required.value,\n      external_content_display: formData.external_content_display.value,\n      check_update: formData.check_update.value,\n    };\n    putSecuritySetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        siteSecurityStore.getState().update(reqParams);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getSecuritySetting().then((setting) => {\n      if (setting) {\n        const formMeta = { ...formData };\n        formMeta.login_required.value = setting.login_required;\n        formMeta.external_content_display.value =\n          setting.external_content_display;\n        formMeta.check_update.value = setting.check_update;\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('security', { keyPrefix: 'nav_menus' })}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onChange={handleValueChange}\n          onSubmit={onSubmit}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Security;\n"
  },
  {
    "path": "ui/src/pages/Admin/Seo/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { getSeoSetting, putSeoSetting } from '@/services';\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport { useToast } from '@/hooks';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { seoSettingStore } from '@/stores';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.seo',\n  });\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      permalink: {\n        type: 'number',\n        title: t('permalink.label'),\n        description: t('permalink.text'),\n        enum: [4, 3, 2, 1],\n        enumNames: [\n          '/questions/D1D1',\n          '/questions/D1D1/post-title',\n          '/questions/10010000000000001',\n          '/questions/10010000000000001/post-title',\n        ],\n        default: 4,\n      },\n      robots: {\n        type: 'string',\n        title: t('robots.label'),\n        description: t('robots.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    permalink: {\n      'ui:widget': 'select',\n    },\n    robots: {\n      'ui:widget': 'textarea',\n      'ui:options': {\n        rows: 10,\n        className: 'font-monospace',\n      },\n    },\n  };\n  const [formData, setFormData] = useState(initFormData(schema));\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: Type.AdminSettingsSeo = {\n      permalink: Number(formData.permalink.value),\n      robots: formData.robots.value,\n    };\n\n    putSeoSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        seoSettingStore.getState().update(reqParams);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getSeoSetting().then((setting) => {\n      if (setting) {\n        const formMeta = { ...formData };\n        formMeta.robots.value = setting.robots;\n        formMeta.permalink.value = setting.permalink;\n        if (!/[1234]/.test(formMeta.permalink.value)) {\n          formMeta.permalink.value = 4;\n        }\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Smtp/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { useSmtpSetting, updateSmtpSetting } from '@/services';\nimport pattern from '@/common/pattern';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Smtp: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.smtp',\n  });\n  const Toast = useToast();\n  const { data: setting } = useSmtpSetting();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      from_email: {\n        type: 'string',\n        title: t('from_email.label'),\n        description: t('from_email.text'),\n      },\n      from_name: {\n        type: 'string',\n        title: t('from_name.label'),\n        description: t('from_name.text'),\n      },\n      smtp_host: {\n        type: 'string',\n        title: t('smtp_host.label'),\n        description: t('smtp_host.text'),\n      },\n      encryption: {\n        type: 'string',\n        title: t('encryption.label'),\n        description: t('encryption.text'),\n        enum: ['TLS', 'SSL', ''],\n        enumNames: ['TLS', 'SSL', 'None'],\n      },\n      smtp_port: {\n        type: 'number',\n        title: t('smtp_port.label'),\n        description: t('smtp_port.text'),\n      },\n      smtp_authentication: {\n        type: 'boolean',\n        title: t('smtp_authentication.title'),\n        enum: [true, false],\n        enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],\n      },\n      smtp_username: {\n        type: 'string',\n        title: t('smtp_username.label'),\n      },\n      smtp_password: {\n        type: 'string',\n        title: t('smtp_password.label'),\n      },\n      test_email_recipient: {\n        type: 'string',\n        title: t('test_email_recipient.label'),\n        description: t('test_email_recipient.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    from_email: {\n      'ui:options': {\n        inputType: 'email',\n      },\n    },\n    encryption: {\n      'ui:widget': 'select',\n    },\n    smtp_username: {\n      'ui:options': {\n        validator: (value: string, formData) => {\n          if (formData.smtp_authentication.value) {\n            if (!value) {\n              return t('smtp_username.msg');\n            }\n          }\n          return true;\n        },\n      },\n    },\n    smtp_password: {\n      'ui:options': {\n        inputType: 'password',\n        validator: (value: string, formData) => {\n          if (formData.smtp_authentication.value) {\n            if (!value) {\n              return t('smtp_password.msg');\n            }\n          }\n          return true;\n        },\n      },\n    },\n    smtp_authentication: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('smtp_authentication.label'),\n      },\n    },\n    smtp_port: {\n      'ui:options': {\n        inputType: 'number',\n        validator: (value) => {\n          if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) {\n            return t('smtp_port.msg');\n          }\n          return true;\n        },\n      },\n    },\n    test_email_recipient: {\n      'ui:options': {\n        inputType: 'email',\n        validator: (value) => {\n          if (value && !pattern.email.test(value)) {\n            return t('test_email_recipient.msg');\n          }\n          return true;\n        },\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    const reqParams: Type.AdminSettingsSmtp = {\n      from_email: formData.from_email.value,\n      from_name: formData.from_name.value,\n      smtp_host: formData.smtp_host.value,\n      encryption: formData.encryption.value,\n      smtp_port: formData.smtp_port.value,\n      smtp_authentication: formData.smtp_authentication.value,\n      ...(formData.smtp_authentication.value\n        ? { smtp_username: formData.smtp_username.value }\n        : {}),\n      ...(formData.smtp_authentication.value\n        ? { smtp_password: formData.smtp_password.value }\n        : {}),\n      test_email_recipient: formData.test_email_recipient.value,\n    };\n\n    updateSmtpSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    if (!setting) {\n      return;\n    }\n    const formMeta = {};\n    Object.keys(setting).forEach((k) => {\n      formMeta[k] = { ...formData[k], value: setting[k] };\n    });\n    setFormData({ ...formData, ...formMeta });\n  }, [setting]);\n\n  useEffect(() => {\n    if (!/true|false/.test(formData.smtp_authentication.value)) {\n      return;\n    }\n    if (formData.smtp_authentication.value) {\n      setFormData({\n        ...formData,\n        smtp_username: { ...formData.smtp_username, hidden: false },\n        smtp_password: { ...formData.smtp_password, hidden: false },\n      });\n    } else {\n      setFormData({\n        ...formData,\n        smtp_username: { ...formData.smtp_username, hidden: true },\n        smtp_password: { ...formData.smtp_password, hidden: true },\n      });\n    }\n  }, [formData.smtp_authentication.value]);\n\n  const handleOnChange = (data) => {\n    setFormData(data);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onChange={handleOnChange}\n          onSubmit={onSubmit}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Smtp;\n"
  },
  {
    "path": "ui/src/pages/Admin/TagsSettings/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n  SchemaForm,\n  JSONSchema,\n  UISchema,\n  initFormData,\n  TabNav,\n} from '@/components';\nimport { ADMIN_TAGS_NAV_MENUS } from '@/common/constants';\nimport * as Type from '@/common/interface';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { writeSettingStore } from '@/stores';\nimport { getAdminTagsSetting, updateAdminTagsSetting } from '@/services/admin';\nimport { useToast } from '@/hooks';\n\nconst QaSettings = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.write',\n  });\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      reserved_tags: {\n        type: 'string',\n        title: t('reserved_tags.label'),\n        description: t('reserved_tags.text'),\n      },\n      recommend_tags: {\n        type: 'string',\n        title: t('recommend_tags.label'),\n        description: t('recommend_tags.text'),\n      },\n      required_tag: {\n        type: 'boolean',\n        title: t('required_tag.title'),\n        description: t('required_tag.text'),\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    reserved_tags: {\n      'ui:widget': 'tag_selector',\n      'ui:options': {\n        label: t('reserved_tags.label'),\n      },\n    },\n    recommend_tags: {\n      'ui:widget': 'tag_selector',\n      'ui:options': {\n        label: t('recommend_tags.label'),\n      },\n    },\n    required_tag: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('required_tag.label'),\n      },\n    },\n  };\n  const [formData, setFormData] = useState<Type.FormDataType>(\n    initFormData(schema),\n  );\n\n  const handleValueChange = (data: Type.FormDataType) => {\n    setFormData(data);\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { recommend_tags, reserved_tags } = formData;\n    // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签\n    // 通过标签中的 slug_name 来去重\n    const repeatTag = recommend_tags.value.filter((tag) =>\n      reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),\n    );\n    if (repeatTag.length > 0) {\n      handleValueChange({\n        ...formData,\n        recommend_tags: {\n          ...recommend_tags,\n          errorMsg: t('recommend_tags.msg.contain_reserved'),\n          isInvalid: true,\n        },\n      });\n      bol = false;\n      const ele = document.getElementById('recommend_tags');\n      scrollToElementTop(ele);\n    } else {\n      handleValueChange({\n        ...formData,\n        recommend_tags: {\n          ...recommend_tags,\n          errorMsg: '',\n          isInvalid: false,\n        },\n      });\n    }\n    return bol;\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    const reqParams: Type.AdminTagsSetting = {\n      recommend_tags: formData.recommend_tags.value,\n      reserved_tags: formData.reserved_tags.value,\n      required_tag: formData.required_tag.value,\n    };\n    updateAdminTagsSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        writeSettingStore.getState().update({ ...reqParams });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getAdminTagsSetting().then((res) => {\n      if (res) {\n        const formMeta = { ...formData };\n        if (Array.isArray(res.recommend_tags)) {\n          formData.recommend_tags.value = res.recommend_tags;\n        } else {\n          formData.recommend_tags.value = [];\n        }\n        if (Array.isArray(res.reserved_tags)) {\n          formData.reserved_tags.value = res.reserved_tags;\n        } else {\n          formData.reserved_tags.value = [];\n        }\n        formMeta.required_tag.value = res.required_tag;\n        setFormData(formMeta);\n      }\n    });\n  }, []);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('tags', { keyPrefix: 'nav_menus' })}</h3>\n      <TabNav menus={ADMIN_TAGS_NAV_MENUS} />\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onChange={handleValueChange}\n          onSubmit={onSubmit}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default QaSettings;\n"
  },
  {
    "path": "ui/src/pages/Admin/Themes/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { getThemeSetting, putThemeSetting } from '@/services';\nimport { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';\nimport { useToast } from '@/hooks';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { themeSettingStore } from '@/stores';\nimport { setupAppTheme } from '@/utils/localize';\nimport { DEFAULT_THEME_COLOR } from '@/common/constants';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.themes',\n  });\n  const Toast = useToast();\n  const [themeSetting, setThemeSetting] = useState<Type.AdminSettingsTheme>();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      themes: {\n        type: 'string',\n        title: t('themes.label'),\n        description: t('themes.text'),\n        enum: themeSetting?.theme_options?.map((_) => _.value),\n        enumNames: themeSetting?.theme_options?.map((_) => _.label),\n        default: themeSetting?.theme_options?.[0]?.value,\n      },\n      layout: {\n        type: 'string',\n        title: t('layout.label'),\n        enum: ['Full-width', 'Fixed-width'],\n        enumNames: [t('layout.full_width'), t('layout.fixed_width')],\n        default: themeSetting?.layout,\n      },\n      color_scheme: {\n        type: 'string',\n        title: t('color_scheme.label'),\n        enum: ['system', 'light', 'dark'],\n        enumNames: [\n          t('system_setting', { keyPrefix: 'btns' }),\n          t('light', { keyPrefix: 'btns' }),\n          t('dark', { keyPrefix: 'btns' }),\n        ],\n        default: themeSetting?.color_scheme,\n      },\n      navbar_style: {\n        type: 'string',\n        title: t('navbar_style.label'),\n        default: DEFAULT_THEME_COLOR,\n      },\n      primary_color: {\n        type: 'string',\n        title: t('primary_color.label'),\n        description: t('primary_color.text'),\n        default: DEFAULT_THEME_COLOR,\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    themes: {\n      'ui:widget': 'select',\n    },\n    color_scheme: {\n      'ui:widget': 'select',\n    },\n    layout: {\n      'ui:widget': 'select',\n    },\n    navbar_style: {\n      'ui:widget': 'input_group',\n      'ui:options': {\n        inputType: 'color',\n        suffixBtnOptions: {\n          text: '',\n          variant: 'outline-secondary',\n          iconName: 'arrow-counterclockwise',\n          actionType: 'click',\n          title: t('reset', { keyPrefix: 'btns' }),\n          // eslint-disable-next-line @typescript-eslint/no-use-before-define\n          clickCallback: () => resetNavbarStyle(),\n        },\n      },\n    },\n    primary_color: {\n      'ui:widget': 'input_group',\n      'ui:options': {\n        inputType: 'color',\n        suffixBtnOptions: {\n          text: '',\n          variant: 'outline-secondary',\n          iconName: 'arrow-counterclockwise',\n          actionType: 'click',\n          title: t('reset', { keyPrefix: 'btns' }),\n          // eslint-disable-next-line @typescript-eslint/no-use-before-define\n          clickCallback: () => resetPrimaryScheme(),\n        },\n      },\n    },\n  };\n\n  const [formData, setFormData] = useState(initFormData(schema));\n  const { update: updateThemeSetting } = themeSettingStore((_) => _);\n\n  const resetNavbarStyle = () => {\n    const formMeta = { ...formData };\n    formMeta.navbar_style.value = DEFAULT_THEME_COLOR;\n    setFormData({ ...formMeta });\n  };\n\n  const resetPrimaryScheme = () => {\n    const formMeta = { ...formData };\n    formMeta.primary_color.value = DEFAULT_THEME_COLOR;\n    setFormData({ ...formMeta });\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    const themeName = formData.themes.value;\n    const reqParams: Type.AdminSettingsTheme = {\n      theme: themeName,\n      color_scheme: formData.color_scheme.value,\n      layout: formData.layout.value,\n      theme_config: {\n        [themeName]: {\n          navbar_style: formData.navbar_style.value,\n          primary_color: formData.primary_color.value,\n        },\n      },\n    };\n\n    putThemeSetting(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        updateThemeSetting(reqParams);\n        setupAppTheme();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    getThemeSetting().then((setting) => {\n      if (setting) {\n        setThemeSetting(setting);\n        const themeName = setting.theme;\n        const themeConfig = setting.theme_config[themeName];\n        const formMeta = { ...formData };\n        formMeta.themes.value = themeName;\n        formMeta.navbar_style.value = themeConfig?.navbar_style.startsWith('#')\n          ? themeConfig?.navbar_style\n          : DEFAULT_THEME_COLOR;\n        formMeta.primary_color.value = themeConfig?.primary_color;\n        formData.color_scheme.value = setting?.color_scheme || 'system';\n        formData.layout.value = setting?.layout || 'Full-width';\n        setFormData({ ...formMeta });\n      }\n    });\n  }, []);\n\n  const handleOnChange = (cd) => {\n    setFormData(cd);\n    const themeConfig = themeSetting?.theme_config[cd.themes.value];\n    if (themeConfig) {\n      themeConfig.navbar_style = cd.navbar_style.value;\n      themeConfig.primary_color = cd.primary_color.value;\n      setThemeSetting({\n        ...themeSetting,\n        theme: themeSetting?.theme,\n        theme_config: themeSetting?.theme_config,\n      });\n    }\n  };\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('page_title')}</h3>\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          formData={formData}\n          onSubmit={onSubmit}\n          uiSchema={uiSchema}\n          onChange={handleOnChange}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Admin/Users/components/Action/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Dropdown } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal, Icon } from '@/components';\nimport {\n  useChangeUserRoleModal,\n  useChangeProfileModal,\n  useChangePasswordModal,\n  useActivationEmailModal,\n  useToast,\n} from '@/hooks';\nimport {\n  updateUserPassword,\n  changeUserStatus,\n  updateUserProfile,\n} from '@/services';\nimport { toastStore } from '@/stores';\n\ninterface Props {\n  showActionPassword?: boolean;\n  showActionStatus?: boolean;\n  showActionRole?: boolean;\n  currentUser;\n  refreshUsers: () => void;\n  showDeleteModal: (val) => void;\n  showSuspenseModal: (val) => void;\n  userData;\n}\n\nconst UserOperation = ({\n  showActionPassword,\n  showActionStatus,\n  showActionRole,\n  currentUser,\n  refreshUsers,\n  showDeleteModal,\n  userData,\n  showSuspenseModal,\n}: Props) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });\n  const Toast = useToast();\n\n  const changeUserRoleModal = useChangeUserRoleModal({\n    callback: () => {\n      Toast.onShow({\n        msg: t('change_user_role', { keyPrefix: 'messages' }),\n        variant: 'success',\n      });\n      refreshUsers?.();\n    },\n  });\n  const changePasswordModal = useChangePasswordModal({\n    onConfirm: (rd) => {\n      return new Promise((resolve, reject) => {\n        updateUserPassword(rd)\n          .then(() => {\n            Toast.onShow({\n              msg: t('update_password', { keyPrefix: 'toast' }),\n              variant: 'success',\n            });\n            resolve(true);\n          })\n          .catch((e) => {\n            reject(e);\n          });\n      });\n    },\n  });\n  const changeProfileModal = useChangeProfileModal(\n    {\n      onConfirm: (rd) => {\n        return new Promise((resolve, reject) => {\n          updateUserProfile(rd)\n            .then(() => {\n              Toast.onShow({\n                msg: t('edit_success', {\n                  keyPrefix: 'admin.edit_profile_modal',\n                }),\n                variant: 'success',\n              });\n              resolve(true);\n              refreshUsers?.();\n            })\n            .catch((e) => {\n              reject(e);\n            });\n        });\n      },\n    },\n    userData,\n  );\n\n  const activationEmailModal = useActivationEmailModal();\n\n  const postUserStatus = (statusType) => {\n    changeUserStatus({\n      user_id: userData.user_id,\n      status: statusType,\n    }).then(() => {\n      toastStore.getState().show({\n        msg: t(`user_${statusType}`, { keyPrefix: 'messages' }),\n        variant: 'success',\n      });\n      refreshUsers?.();\n      // onClose();\n    });\n  };\n\n  const handleAction = (type) => {\n    const { user_id, role_id, username } = userData;\n    if (username === currentUser.username) {\n      Toast.onShow({\n        msg: t('forbidden_operate_self', { keyPrefix: 'toast' }),\n        variant: 'warning',\n      });\n      return;\n    }\n\n    if (type === 'role') {\n      changeUserRoleModal.onShow({\n        id: user_id,\n        role_id,\n      });\n    }\n\n    if (type === 'password') {\n      changePasswordModal.onShow(user_id);\n    }\n\n    if (type === 'profile') {\n      changeProfileModal.onShow(user_id);\n    }\n\n    if (type === 'activation') {\n      activationEmailModal.onShow(user_id);\n    }\n\n    if (type === 'deactivate') {\n      // cons\n      Modal.confirm({\n        title: t('deactivate_user.title'),\n        content: t('deactivate_user.content'),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        cancelText: t('cancel', { keyPrefix: 'btns' }),\n        confirmText: t('deactivate', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          // active -> inactive\n          postUserStatus('inactive');\n        },\n      });\n    }\n\n    if (type === 'suspend') {\n      // cons\n      showSuspenseModal({\n        show: true,\n        userId: userData.user_id,\n      });\n    }\n\n    if (type === 'active' || type === 'unsuspend') {\n      // to normal\n      postUserStatus('normal');\n    }\n\n    if (type === 'delete') {\n      showDeleteModal({\n        show: true,\n        userId: userData.user_id,\n      });\n    }\n  };\n\n  return (\n    <td className=\"text-end\">\n      <Dropdown>\n        <Dropdown.Toggle variant=\"link\" className=\"no-toggle p-0\">\n          <Icon name=\"three-dots-vertical\" title={t('action')} />\n        </Dropdown.Toggle>\n        <Dropdown.Menu align=\"end\">\n          {showActionPassword ? (\n            <Dropdown.Item onClick={() => handleAction('password')}>\n              {t('set_new_password')}\n            </Dropdown.Item>\n          ) : null}\n          <Dropdown.Item onClick={() => handleAction('profile')}>\n            {t('edit_profile')}\n          </Dropdown.Item>\n          {showActionRole ? (\n            <Dropdown.Item onClick={() => handleAction('role')}>\n              {t('change_role')}\n            </Dropdown.Item>\n          ) : null}\n          {userData.status === 'inactive' ? (\n            <Dropdown.Item onClick={() => handleAction('activation')}>\n              {t('btn_name', { keyPrefix: 'inactive' })}\n            </Dropdown.Item>\n          ) : null}\n          {showActionStatus && userData.status !== 'deleted' ? (\n            <>\n              <Dropdown.Divider />\n              {userData.status === 'inactive' && (\n                <Dropdown.Item onClick={() => handleAction('active')}>\n                  {t('active', { keyPrefix: 'btns' })}\n                </Dropdown.Item>\n              )}\n              {userData.status === 'normal' && (\n                <Dropdown.Item onClick={() => handleAction('deactivate')}>\n                  {t('deactivate', { keyPrefix: 'btns' })}\n                </Dropdown.Item>\n              )}\n              {userData.status === 'normal' && (\n                <Dropdown.Item onClick={() => handleAction('suspend')}>\n                  {t('suspend', { keyPrefix: 'btns' })}\n                </Dropdown.Item>\n              )}\n              {userData.status === 'suspended' && (\n                <Dropdown.Item onClick={() => handleAction('unsuspend')}>\n                  {t('unsuspend', { keyPrefix: 'btns' })}\n                </Dropdown.Item>\n              )}\n              <Dropdown.Item onClick={() => handleAction('delete')}>\n                {t('delete', { keyPrefix: 'btns' })}\n              </Dropdown.Item>\n            </>\n          ) : null}\n        </Dropdown.Menu>\n      </Dropdown>\n    </td>\n  );\n};\n\nexport default UserOperation;\n"
  },
  {
    "path": "ui/src/pages/Admin/Users/components/DeleteUserModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { Modal, Button, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nconst DeleteUserModal = ({ show, onClose, onDelete }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });\n  const [checkVal, setCheckVal] = useState(false);\n\n  const handleClose = () => {\n    onClose();\n    setCheckVal(false);\n  };\n\n  return (\n    <Modal show={show} onHide={handleClose}>\n      <Modal.Header closeButton>\n        <Modal.Title>{t('delete_user.title')}</Modal.Title>\n      </Modal.Header>\n      <Modal.Body>\n        <p>{t('delete_user.content')}</p>\n        <div className=\"text-danger mb-2\">\n          {t('delete_user.remove')} {t('optional', { keyPrefix: 'form' })}\n        </div>\n        <Form>\n          <Form.Group controlId=\"delete_user\">\n            <Form.Check type=\"checkbox\" id=\"delete_user\">\n              <Form.Check.Input\n                type=\"checkbox\"\n                checked={checkVal}\n                onChange={(e) => {\n                  setCheckVal(e.target.checked);\n                }}\n              />\n              <Form.Check.Label htmlFor=\"delete_user\">\n                <span>{t('delete_user.label')}</span>\n                <br />\n                <span className=\"small text-secondary\">\n                  {t('delete_user.text')}\n                </span>\n              </Form.Check.Label>\n            </Form.Check>\n          </Form.Group>\n        </Form>\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={handleClose}>\n          {t('cancel', { keyPrefix: 'btns' })}\n        </Button>\n        <Button variant=\"danger\" onClick={() => onDelete(checkVal)}>\n          {t('delete', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default DeleteUserModal;\n"
  },
  {
    "path": "ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { Modal, Button, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { changeUserStatus } from '@/services';\nimport { SUSPENSE_USER_TIME } from '@/common/constants';\nimport { toastStore } from '@/stores';\n\nconst SuspenseUserModal = ({ show, userId, onClose, refreshUsers }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });\n  const [checkVal, setCheckVal] = useState('forever');\n\n  const handleClose = () => {\n    onClose();\n    setCheckVal('forever');\n  };\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    changeUserStatus({\n      user_id: userId,\n      status: 'suspended',\n      suspend_duration: checkVal,\n    }).then(() => {\n      toastStore.getState().show({\n        msg: t('user_suspended', { keyPrefix: 'messages' }),\n        variant: 'success',\n      });\n      refreshUsers?.();\n      handleClose();\n    });\n  };\n\n  return (\n    <Modal show={show} onHide={handleClose}>\n      <Modal.Header closeButton>\n        <Modal.Title>{t('suspend_user.title')}</Modal.Title>\n      </Modal.Header>\n      <Modal.Body>\n        <p>{t('suspend_user.content')}</p>\n        <Form>\n          <Form.Group controlId=\"delete_user\" className=\"mb-3\">\n            <Form.Label>{t('suspend_user.label')}</Form.Label>\n            <Form.Select\n              value={checkVal}\n              onChange={(e) => setCheckVal(e.target.value)}>\n              <option value=\"forever\">{t('suspend_user.forever')}</option>\n              {SUSPENSE_USER_TIME.map((item) => {\n                return (\n                  <option key={item.value} value={item.value}>\n                    {item.time} {t(item.label, { keyPrefix: 'dates' })}\n                  </option>\n                );\n              })}\n            </Form.Select>\n          </Form.Group>\n        </Form>\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={handleClose}>\n          {t('cancel', { keyPrefix: 'btns' })}\n        </Button>\n        <Button variant=\"danger\" onClick={handleSubmit}>\n          {t('suspend', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default SuspenseUserModal;\n"
  },
  {
    "path": "ui/src/pages/Admin/Users/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Form, Table, Button, Stack } from 'react-bootstrap';\nimport { useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\nimport dayjs from 'dayjs';\n\nimport {\n  Pagination,\n  FormatTime,\n  BaseUserCard,\n  Empty,\n  QueryGroup,\n  Modal,\n  TabNav,\n} from '@/components';\nimport * as Type from '@/common/interface';\nimport { useUserModal } from '@/hooks';\nimport { toastStore, loggedUserInfoStore, userCenterStore } from '@/stores';\nimport {\n  useQueryUsers,\n  addUsers,\n  getAdminUcAgent,\n  AdminUcAgent,\n  changeUserStatus,\n  deletePermanently,\n} from '@/services';\nimport { formatCount } from '@/utils';\nimport { ADMIN_USERS_NAV_MENUS } from '@/common/constants';\n\nimport DeleteUserModal from './components/DeleteUserModal';\nimport Action from './components/Action';\nimport SuspenseUserModal from './components/SuspenseUserModal';\n\nconst UserFilterKeys: Type.UserFilterBy[] = [\n  'normal',\n  'staff',\n  'inactive',\n  'suspended',\n  'deleted',\n];\n\nconst bgMap = {\n  normal: 'text-bg-success',\n  suspended: 'text-bg-danger',\n  deleted: 'text-bg-danger',\n  inactive: 'text-bg-secondary',\n};\n\nconst PAGE_SIZE = 10;\nconst Users: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });\n  const [deleteUserModalState, setDeleteUserModalState] = useState({\n    show: false,\n    userId: '',\n  });\n  const [suspenseUserModalState, setSuspenseUserModalState] = useState({\n    show: false,\n    userId: '',\n  });\n  const [urlSearchParams, setUrlSearchParams] = useSearchParams();\n  const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];\n  const curPage = Number(urlSearchParams.get('page') || '1');\n  const curQuery = urlSearchParams.get('query') || '';\n  const currentUser = loggedUserInfoStore((state) => state.user);\n  const { agent: ucAgent } = userCenterStore();\n  const [adminUcAgent, setAdminUcAgent] = useState<AdminUcAgent>({\n    allow_create_user: true,\n    allow_update_user_status: true,\n    allow_update_user_password: true,\n    allow_update_user_role: true,\n  });\n\n  const {\n    data,\n    isLoading,\n    mutate: refreshUsers,\n  } = useQueryUsers({\n    page: curPage,\n    page_size: PAGE_SIZE,\n    query: curQuery,\n    ...(curFilter === 'all'\n      ? {}\n      : curFilter === 'staff'\n        ? { staff: true }\n        : { status: curFilter }),\n  });\n\n  const userModal = useUserModal({\n    onConfirm: (userModel) => {\n      return new Promise((resolve, reject) => {\n        addUsers(userModel)\n          .then(() => {\n            toastStore.getState().show({\n              msg: t('user_added', { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            urlSearchParams.set('filter', 'normal');\n            urlSearchParams.delete('page');\n            setUrlSearchParams(urlSearchParams);\n            refreshUsers();\n            resolve(true);\n          })\n          .catch((e) => {\n            reject(e);\n          });\n      });\n    },\n  });\n\n  const handleFilter = (e) => {\n    urlSearchParams.set('query', e.target.value);\n    urlSearchParams.delete('page');\n    setUrlSearchParams(urlSearchParams);\n  };\n  useEffect(() => {\n    if (ucAgent?.enabled) {\n      getAdminUcAgent().then((resp) => {\n        setAdminUcAgent(resp);\n      });\n    }\n  }, [ucAgent]);\n\n  const changeDeleteUserModalState = (modalData: {\n    show: boolean;\n    userId: string;\n  }) => {\n    setDeleteUserModalState(modalData);\n  };\n\n  const handleDelete = (val) => {\n    changeUserStatus({\n      user_id: deleteUserModalState.userId,\n      status: 'deleted',\n      remove_all_content: val,\n    }).then(() => {\n      toastStore.getState().show({\n        msg: t('user_deleted', { keyPrefix: 'messages' }),\n        variant: 'success',\n      });\n      changeDeleteUserModalState({\n        show: false,\n        userId: '',\n      });\n      refreshUsers();\n    });\n  };\n\n  const handleDeletePermanently = () => {\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'delete_permanently' }),\n      content: t('content', { keyPrefix: 'delete_permanently' }),\n      cancelBtnVariant: 'link',\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      confirmBtnVariant: 'danger',\n      onConfirm: () => {\n        deletePermanently('users').then(() => {\n          toastStore.getState().show({\n            msg: t('users_deleted', { keyPrefix: 'messages' }),\n            variant: 'success',\n          });\n          refreshUsers();\n        });\n      },\n    });\n  };\n\n  const handleSuspenseUserModalState = (modalData: {\n    show: boolean;\n    userId: string;\n  }) => {\n    setSuspenseUserModalState(modalData);\n  };\n\n  const showAddUser =\n    !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);\n  const showActionPassword =\n    !ucAgent?.enabled ||\n    (ucAgent?.enabled && adminUcAgent?.allow_update_user_password);\n\n  const showActionRole =\n    !ucAgent?.enabled ||\n    (ucAgent?.enabled && adminUcAgent?.allow_update_user_role);\n\n  const showActionStatus =\n    !ucAgent?.enabled ||\n    (ucAgent?.enabled && adminUcAgent?.allow_update_user_status);\n  const showAction = showActionPassword || showActionRole || showActionStatus;\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <TabNav menus={ADMIN_USERS_NAV_MENUS} />\n      <div className=\"d-flex flex-wrap justify-content-between align-items-center\">\n        <Stack direction=\"horizontal\" gap={3} className=\"mb-3\">\n          <QueryGroup\n            data={UserFilterKeys}\n            currentSort={curFilter}\n            sortKey=\"filter\"\n            i18nKeyPrefix=\"admin.users\"\n          />\n          {curFilter === 'deleted' && Number(data?.count) > 0 ? (\n            <Button\n              variant=\"outline-danger\"\n              size=\"sm\"\n              onClick={() => handleDeletePermanently()}>\n              {t('deleted_permanently', { keyPrefix: 'btns' })}\n            </Button>\n          ) : null}\n          {showAddUser ? (\n            <Button\n              variant=\"outline-primary\"\n              size=\"sm\"\n              onClick={() => userModal.onShow()}>\n              {t('add_user')}\n            </Button>\n          ) : null}\n        </Stack>\n\n        <Form.Control\n          size=\"sm\"\n          type=\"search\"\n          value={curQuery}\n          onChange={handleFilter}\n          placeholder={t('filter.placeholder')}\n          style={{ width: '12.25rem' }}\n          className=\"mb-3\"\n        />\n      </div>\n      <Table responsive=\"md\">\n        <thead>\n          <tr>\n            <th>{t('name')}</th>\n            <th style={{ width: '12%' }}>{t('reputation')}</th>\n            <th style={{ width: '15%' }} className=\"min-w-15\">\n              {t('email')}\n            </th>\n            <th className=\"text-nowrap\" style={{ width: '12%' }}>\n              {t('created_at')}\n            </th>\n            {(curFilter === 'deleted' || curFilter === 'suspended') && (\n              <th className=\"text-nowrap\" style={{ width: '12%' }}>\n                {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')}\n              </th>\n            )}\n            {curFilter === 'suspended' && (\n              <th className=\"text-nowrap\" style={{ width: '12%' }}>\n                {t('suspend_until')}\n              </th>\n            )}\n\n            <th style={{ width: '12%' }}>{t('status')}</th>\n            {curFilter !== 'suspended' && curFilter !== 'deleted' && (\n              <th style={{ width: '12%' }}>{t('role')}</th>\n            )}\n            {curFilter !== 'deleted' ? (\n              <th style={{ width: '8%' }} className=\"text-end\">\n                {t('action')}\n              </th>\n            ) : null}\n          </tr>\n        </thead>\n        <tbody className=\"align-middle\">\n          {data?.list.map((user) => {\n            return (\n              <tr key={user.user_id}>\n                <td>\n                  <BaseUserCard\n                    data={user}\n                    className=\"fs-6\"\n                    avatarSize=\"32px\"\n                    avatarSearchStr=\"s=48\"\n                    avatarClass=\"me-2\"\n                    showReputation={false}\n                    nameMaxWidth=\"160px\"\n                  />\n                </td>\n                <td>{formatCount(user.rank)}</td>\n                <td className=\"text-break\">{user.e_mail}</td>\n                <td>\n                  <FormatTime time={user.created_at} />\n                </td>\n                {curFilter === 'suspended' && (\n                  <>\n                    <td className=\"text-nowrap\">\n                      <FormatTime time={user.suspended_at} />\n                    </td>\n                    <td className=\"text-nowrap\">\n                      {user.suspended_until <= 0 ||\n                      Number(\n                        dayjs(user.suspended_until * 1000).format('YYYY'),\n                      ) > 2099\n                        ? t('suspend_user.forever')\n                        : dayjs(user.suspended_until * 1000).format(\n                            t('long_date_with_time', { keyPrefix: 'dates' }),\n                          )}\n                    </td>\n                  </>\n                )}\n                {curFilter === 'deleted' && (\n                  <td className=\"text-nowrap\">\n                    <FormatTime time={user.deleted_at} />\n                  </td>\n                )}\n                <td>\n                  <span className={classNames('badge', bgMap[user.status])}>\n                    {t(user.status)}\n                  </span>\n                </td>\n                {curFilter !== 'suspended' && curFilter !== 'deleted' && (\n                  <td>\n                    <span className=\"badge text-bg-light\">\n                      {t(user.role_name)}\n                    </span>\n                  </td>\n                )}\n                {curFilter !== 'deleted' &&\n                (showAction || user.status === 'inactive') ? (\n                  <Action\n                    userData={user}\n                    showActionPassword={showActionPassword}\n                    showActionRole={showActionRole}\n                    showActionStatus={showActionStatus}\n                    currentUser={currentUser}\n                    refreshUsers={refreshUsers}\n                    showDeleteModal={changeDeleteUserModalState}\n                    showSuspenseModal={handleSuspenseUserModalState}\n                  />\n                ) : null}\n              </tr>\n            );\n          })}\n        </tbody>\n      </Table>\n      {Number(data?.count) <= 0 && !isLoading && <Empty />}\n      <div className=\"mt-4 mb-2 d-flex justify-content-center\">\n        <Pagination\n          currentPage={curPage}\n          totalSize={data?.count || 0}\n          pageSize={PAGE_SIZE}\n        />\n      </div>\n\n      <DeleteUserModal\n        show={deleteUserModalState.show}\n        onClose={() => {\n          changeDeleteUserModalState({\n            show: false,\n            userId: '',\n          });\n        }}\n        onDelete={(val) => handleDelete(val)}\n      />\n      <SuspenseUserModal\n        show={suspenseUserModalState.show}\n        userId={suspenseUserModalState.userId}\n        onClose={() => {\n          handleSuspenseUserModalState({\n            show: false,\n            userId: '',\n          });\n        }}\n        refreshUsers={refreshUsers}\n      />\n    </>\n  );\n};\n\nexport default Users;\n"
  },
  {
    "path": "ui/src/pages/Admin/UsersSettings/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { SchemaForm, JSONSchema, UISchema, TabNav } from '@/components';\nimport {\n  ADMIN_USERS_NAV_MENUS,\n  SYSTEM_AVATAR_OPTIONS,\n} from '@/common/constants';\nimport { FormDataType } from '@/common/interface';\nimport { useAdminUsersSettings, updateAdminUsersSettings } from '@/services';\nimport { useToast } from '@/hooks';\nimport { siteInfoStore } from '@/stores';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst UsersSettings = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'admin.interface',\n  });\n  const { data: setting } = useAdminUsersSettings();\n  const Toast = useToast();\n  const schema: JSONSchema = {\n    title: t('page_title'),\n    properties: {\n      default_avatar: {\n        type: 'string',\n        title: t('avatar.label'),\n        description: t('avatar.text'),\n        enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),\n        enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),\n        default: setting?.default_avatar || 'system',\n      },\n      gravatar_base_url: {\n        type: 'string',\n        title: t('gravatar_base_url.label'),\n        description: t('gravatar_base_url.text'),\n        default: setting?.gravatar_base_url || '',\n      },\n    },\n  };\n\n  const [formData, setFormData] = useState<FormDataType>({\n    default_avatar: {\n      value: setting?.default_avatar || 'system',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    gravatar_base_url: {\n      value: setting?.gravatar_base_url || '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const uiSchema: UISchema = {\n    default_avatar: {\n      'ui:widget': 'select',\n    },\n    gravatar_base_url: {\n      'ui:widget': 'input',\n      'ui:options': {\n        placeholder: 'https://www.gravatar.com/avatar/',\n      },\n    },\n  };\n\n  const handleValueChange = (data: FormDataType) => {\n    setFormData(data);\n  };\n\n  const onSubmit = (evt) => {\n    evt.preventDefault();\n    evt.stopPropagation();\n    const reqParams = {\n      default_avatar: formData.default_avatar.value,\n      gravatar_base_url: formData.gravatar_base_url.value,\n    };\n    updateAdminUsersSettings(reqParams)\n      .then(() => {\n        Toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        siteInfoStore.getState().updateUsers({\n          ...siteInfoStore.getState().users,\n          ...reqParams,\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  useEffect(() => {\n    if (setting) {\n      const formMeta = {};\n      Object.keys(setting).forEach((k) => {\n        let v = setting[k];\n        if (k === 'default_avatar' && !v) {\n          v = 'system';\n        }\n        if (k === 'gravatar_base_url' && !v) {\n          v = '';\n        }\n        formMeta[k] = { ...formData[k], value: v };\n      });\n      setFormData({ ...formData, ...formMeta });\n    }\n  }, [setting]);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('users', { keyPrefix: 'nav_menus' })}</h3>\n      <TabNav menus={ADMIN_USERS_NAV_MENUS} />\n      <div className=\"max-w-748\">\n        <SchemaForm\n          schema={schema}\n          uiSchema={uiSchema}\n          formData={formData}\n          onChange={handleValueChange}\n          onSubmit={onSubmit}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default UsersSettings;\n"
  },
  {
    "path": "ui/src/pages/Admin/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.min-w-15 {\n  min-width: 15rem;\n}\n\n.max-w-30 {\n  max-width: 30rem;\n}\n\n.max-w-748 {\n  max-width: 748px;\n}\n\n@media screen and (max-width: 768px) {\n  .max-w-30 {\n    max-width: 15rem;\n  }\n}\n\n.table tr th {\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "ui/src/pages/Admin/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Row, Col } from 'react-bootstrap';\nimport { Outlet } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { AdminSideNav, Footer } from '@/components';\n\nimport '@/common/sideNavLayout.scss';\nimport './index.scss';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_title' });\n\n  usePageTags({\n    title: t('admin'),\n  });\n  return (\n    <div className=\"admin-container d-flex\">\n      <div\n        className=\"position-sticky px-3 border-end pt-4 d-none d-xl-block\"\n        id=\"pcSideNav\">\n        <AdminSideNav />\n      </div>\n      <div className=\"flex-fill w-100\">\n        <div className=\"d-flex justify-content-center px-0 px-md-4\">\n          <div className=\"answer-container main-mx-with\">\n            <Row className=\"py-4\">\n              <Col className=\"page-main flex-auto\">\n                <Outlet />\n              </Col>\n            </Row>\n          </div>\n        </div>\n        <div className=\"d-flex justify-content-center px-0 px-md-4\">\n          <div className=\"main-mx-with\">\n            <Footer />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/AiAssistant/components/ConversationList/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Card, ListGroup } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\ninterface ConversationListItem {\n  conversation_id: string;\n  topic: string;\n}\n\ninterface IProps {\n  data: {\n    count: number;\n    list: ConversationListItem[];\n  };\n  loadMore: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\nconst Index: FC<IProps> = ({ data, loadMore }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });\n\n  if (Number(data?.list.length) <= 0) return null;\n  return (\n    <Card>\n      <Card.Header>\n        <span>{t('recent_conversations')}</span>\n      </Card.Header>\n      <ListGroup variant=\"flush\">\n        {data?.list.map((item) => {\n          return (\n            <ListGroup.Item\n              as={Link}\n              action\n              key={item.conversation_id}\n              to={`/ai-assistant/${item.conversation_id}`}\n              className=\"text-truncate\">\n              {item.topic}\n            </ListGroup.Item>\n          );\n        })}\n        {Number(data?.count) > data?.list.length && (\n          <ListGroup.Item action onClick={loadMore} className=\"link-primary\">\n            {t('show_more')}\n          </ListGroup.Item>\n        )}\n      </ListGroup>\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/AiAssistant/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Row, Col, Spinner, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useParams, useNavigate } from 'react-router-dom';\n\nimport classNames from 'classnames';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport * as Type from '@/common/interface';\nimport requestAi, { cancelCurrentRequest } from '@/utils/requestAi';\nimport { Sender, BubbleUser, BubbleAi, Icon } from '@/components';\nimport { getConversationDetail, getConversationList } from '@/services';\nimport { usePageTags } from '@/hooks';\nimport { Storage } from '@/utils';\n\nimport ConversationsList from './components/ConversationList';\n\ninterface ConversationListItem {\n  conversation_id: string;\n  topic: string;\n}\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });\n  const [isShowConversationList, setIsShowConversationList] = useState(false);\n  const [isGenerate, setIsGenerate] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const [recentNewItem, setRecentNewItem] = useState<any>(null);\n  const [conversions, setConversions] = useState<Type.ConversationDetail>({\n    records: [],\n    conversation_id: '',\n    created_at: 0,\n    topic: '',\n    updated_at: 0,\n  });\n  const navigate = useNavigate();\n  const { id = '' } = useParams<{ id: string }>();\n  const [temporaryBottomSpace, setTemporaryBottomSpace] = useState(0);\n  const [conversationsPage, setConversationsPage] = useState(1);\n\n  const [conversationsList, setConversationsList] = useState<{\n    count: number;\n    list: ConversationListItem[];\n  }>({\n    count: 0,\n    list: [],\n  });\n\n  const calculateTemporarySpace = () => {\n    const viewportHeight = window.innerHeight;\n    const navHeight = 64;\n    const senderHeight = (document.querySelector('.sender-wrap') as HTMLElement)\n      ?.offsetHeight;\n    const neededSpace = viewportHeight - senderHeight - navHeight - 120;\n    const height = neededSpace;\n    console.log('lasMsgHeight', height);\n\n    setTemporaryBottomSpace(height);\n  };\n\n  const resetPageState = () => {\n    setConversions({\n      records: [],\n      conversation_id: '',\n      created_at: 0,\n      topic: '',\n      updated_at: 0,\n    });\n    setIsGenerate(false);\n    setRecentNewItem(null);\n  };\n\n  const handleNewConversation = (e) => {\n    e.preventDefault();\n    navigate('/ai-assistant', { replace: true });\n  };\n\n  const fetchDetail = () => {\n    getConversationDetail(id).then((res) => {\n      setConversions(res);\n    });\n  };\n\n  const handleSubmit = async (userMsg) => {\n    setIsLoading(true);\n    if (conversions?.records.length === 0) {\n      setRecentNewItem({\n        conversation_id: id,\n        topic: userMsg,\n      });\n    }\n    const chatId = Date.now();\n    setConversions((prev) => ({\n      ...prev,\n      topic: userMsg,\n      conversation_id: id,\n      records: [\n        ...prev.records,\n        {\n          id: chatId,\n          role: 'user',\n          content: userMsg,\n          chat_completion_id: String(chatId), // Add required properties\n          helpful: 0,\n          unhelpful: 0,\n          created_at: chatId,\n        },\n      ],\n    }));\n\n    // scroll to user message after the page height is stable\n    requestAnimationFrame(() => {\n      const userBubbles = document.querySelectorAll('.bubble-user-wrap');\n      const lastUserBubble = userBubbles[userBubbles.length - 1];\n      if (lastUserBubble) {\n        lastUserBubble.scrollIntoView({\n          behavior: 'smooth',\n          block: 'start',\n        });\n      }\n    });\n\n    calculateTemporarySpace();\n\n    const params = {\n      conversation_id: id,\n      messages: [\n        {\n          role: 'user',\n          content: userMsg,\n        },\n      ],\n    };\n\n    await requestAi('/answer/api/v1/chat/completions', {\n      body: JSON.stringify(params),\n      onMessage: (res) => {\n        if (!res.choices[0].delta?.content) {\n          return;\n        }\n        setIsLoading(false);\n        setIsGenerate(true);\n        setConversions((prev) => {\n          const updatedRecords = [...prev.records];\n          const lastConversion = updatedRecords[updatedRecords.length - 1];\n          if (lastConversion?.chat_completion_id === res?.chat_completion_id) {\n            updatedRecords[updatedRecords.length - 1] = {\n              ...lastConversion,\n              content: lastConversion.content + res.choices[0].delta.content,\n            };\n          } else {\n            updatedRecords.push({\n              chat_completion_id: res.chat_completion_id,\n              role: res.choices[0].delta.role || 'assistant',\n              content: res.choices[0].delta.content,\n              helpful: 0,\n              unhelpful: 0,\n              created_at: Date.now(),\n            });\n          }\n          return {\n            ...prev,\n            conversation_id: params.conversation_id,\n            records: updatedRecords,\n          };\n        });\n      },\n      onError: (error) => {\n        setIsLoading(false);\n        setIsGenerate(false);\n        console.error('Error:', error);\n      },\n      onComplete: () => {\n        setIsGenerate(false);\n        setIsLoading(false);\n      },\n    });\n  };\n\n  const handleSender = (userMsg) => {\n    if (conversions?.records.length <= 0) {\n      const newConversationId = uuidv4();\n      navigate(`/ai-assistant/${newConversationId}`);\n      Storage.set('_a_once_msg', userMsg);\n    } else {\n      handleSubmit(userMsg);\n    }\n  };\n\n  const handleCancel = () => {\n    if (cancelCurrentRequest()) {\n      setIsGenerate(false);\n    }\n  };\n\n  usePageTags({\n    title: conversions?.topic || t('ai_assistant', { keyPrefix: 'page_title' }),\n  });\n\n  useEffect(() => {\n    if (id) {\n      const msg = Storage.get('_a_once_msg');\n      Storage.remove('_a_once_msg');\n      if (msg) {\n        if (msg) {\n          handleSubmit(msg);\n        }\n        return;\n      }\n      fetchDetail();\n    } else {\n      resetPageState();\n    }\n  }, [id]);\n\n  const getList = (p) => {\n    getConversationList({\n      page: p,\n      page_size: 10,\n    }).then((res) => {\n      setConversationsList({\n        count: res.count,\n        list: [...conversationsList.list, ...res.list],\n      });\n    });\n  };\n\n  const getMore = (e) => {\n    e.preventDefault();\n    setConversationsPage((prev) => prev + 1);\n    getList(conversationsPage + 1);\n  };\n\n  useEffect(() => {\n    getList(1);\n\n    return () => {\n      setConversationsList({\n        count: 0,\n        list: [],\n      });\n      setConversationsPage(1);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (recentNewItem && recentNewItem.conversation_id) {\n      setConversationsList((prev) => ({\n        ...prev,\n        list: [\n          recentNewItem,\n          ...prev.list.filter(\n            (item) => item.conversation_id !== recentNewItem.conversation_id,\n          ),\n        ],\n      }));\n    }\n  }, [recentNewItem]);\n\n  return (\n    <div className=\"pt-4 d-flex flex-column flex-grow-1 position-relative\">\n      <div className=\"d-flex justify-content-between align-items-center mb-4\">\n        <h3 className=\"mb-0\">\n          {t('ai_assistant', { keyPrefix: 'page_title' })}\n        </h3>\n        <div>\n          <Button\n            variant=\"outline-primary\"\n            href=\"/ai-assistant\"\n            className=\"me-2\"\n            size=\"sm\"\n            onClick={handleNewConversation}>\n            {t('new')}\n          </Button>\n          <Button\n            variant={isShowConversationList ? 'secondary' : 'outline-secondary'}\n            size=\"sm\"\n            title={t('recent_conversations')}\n            onClick={() => setIsShowConversationList(!isShowConversationList)}>\n            <Icon name=\"clock-history\" />\n          </Button>\n        </div>\n      </div>\n      <Row\n        className={classNames(\n          'flex-grow-1',\n          !isShowConversationList ? 'justify-content-center' : '',\n        )}>\n        <Col\n          className={classNames(\n            'page-main flex-auto d-flex flex-column flex-grow-1',\n            !conversions?.conversation_id ? 'justify-content-center' : '',\n          )}\n          style={{ maxWidth: '772px' }}>\n          {conversions?.records.length > 0 && (\n            <div className=\"flex-grow-1 pb-5\">\n              {conversions?.records.map((item, index) => {\n                const isLastMessage =\n                  index === Number(conversions?.records.length) - 1;\n                return (\n                  <div\n                    key={`${item.chat_completion_id}-${item.role}`}\n                    className={`${isLastMessage ? '' : 'mb-4'}`}>\n                    {item.role === 'user' ? (\n                      <BubbleUser content={item.content} />\n                    ) : (\n                      <BubbleAi\n                        minHeight={isLastMessage ? temporaryBottomSpace : 0}\n                        canType={isGenerate && isLastMessage}\n                        chatId={item.chat_completion_id}\n                        isLast={isLastMessage}\n                        isCompleted={!isGenerate}\n                        content={item.content}\n                        actionData={{\n                          helpful: item.helpful,\n                          unhelpful: item.unhelpful,\n                        }}\n                      />\n                    )}\n                  </div>\n                );\n              })}\n\n              {temporaryBottomSpace > 0 && isLoading && (\n                <div\n                  style={{\n                    height: `${temporaryBottomSpace}px`,\n                  }}>\n                  {isLoading && (\n                    <Spinner\n                      animation=\"border\"\n                      size=\"sm\"\n                      variant=\"secondary\"\n                      className=\"mt-4\"\n                    />\n                  )}\n                </div>\n              )}\n            </div>\n          )}\n          {conversions?.conversation_id ? null : (\n            <h5 className=\"text-center mb-3\">{t('description')}</h5>\n          )}\n          <Sender\n            onSubmit={handleSender}\n            onCancel={handleCancel}\n            isGenerate={isGenerate || isLoading}\n            hasConversation={!!conversions?.conversation_id}\n          />\n        </Col>\n        {isShowConversationList && (\n          <Col className=\"page-right-side mt-4 mt-xl-0\">\n            <ConversationsList data={conversationsList} loadMore={getMore} />\n          </Col>\n        )}\n      </Row>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Badges/Detail/components/Badge/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, Badge } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport * as Type from '@/common/interface';\nimport { Icon } from '@/components';\nimport { formatCount } from '@/utils';\n\ninterface IProps {\n  data: Type.BadgeInfo;\n}\n\nconst Index: FC<IProps> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges' });\n\n  if (!data?.id) {\n    return null;\n  }\n\n  return (\n    <Card className=\"mb-4\">\n      <Card.Body className=\"d-flex\">\n        {data.icon?.startsWith('http') ? (\n          <img\n            src={data.icon}\n            width={96}\n            height={96}\n            alt={data.name}\n            className=\"me-3\"\n          />\n        ) : (\n          <Icon\n            name={data?.icon}\n            size=\"96px\"\n            className={classnames(\n              'lh-1 me-3',\n              data?.level === 1 && 'bronze',\n              data?.level === 2 && 'silver',\n              data?.level === 3 && 'gold',\n            )}\n          />\n        )}\n        <div>\n          <h5>{data.name}</h5>\n          <div dangerouslySetInnerHTML={{ __html: data.description || '' }} />\n\n          {!data.is_single && (\n            <div className=\"mt-2\">{t('can_earn_multiple')}</div>\n          )}\n\n          {(data.award_count > 0 || data.earned_count > 0) && (\n            <div className=\"small mt-2\">\n              {data.award_count > 0 && (\n                <span className=\"text-secondary me-2\">\n                  {t('×_awarded', { number: formatCount(data.award_count) })}\n                </span>\n              )}\n\n              {data.earned_count > 1 && (\n                <Badge bg=\"success\">\n                  {t('earned_×', { number: data.earned_count })}\n                </Badge>\n              )}\n              {data.earned_count === 1 && (\n                <Badge bg=\"success\">{t('earned')}</Badge>\n              )}\n            </div>\n          )}\n        </div>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Card } from 'react-bootstrap';\n\nconst Index = () => {\n  return (\n    <Card className=\"mb-4 placeholder-glow\">\n      <Card.Body className=\"d-block d-sm-flex\">\n        <div\n          className=\"placeholder me-3 flex-shrink-0\"\n          style={{ width: '96px', height: '96px' }}\n        />\n\n        <div className=\"w-100 mt-3 mt-sm-0\">\n          <div className=\"placeholder h5 w-25\" />\n          <div className=\"placeholder w-100\" />\n          <div className=\"placeholder w-75\" />\n\n          <div className=\"placeholder mt-2 w-50\" />\n\n          <div className=\"small mt-2\">\n            <span className=\"placeholder\" style={{ width: '80px' }} />\n\n            <span className=\"placeholder ms-2\" style={{ width: '80px' }} />\n          </div>\n        </div>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Badges/Detail/components/Loader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Col } from 'react-bootstrap';\n\ninterface Props {\n  count?: number;\n}\n\nconst Index: FC<Props> = ({ count = 12 }) => {\n  const list = new Array(count).fill(0).map((v, i) => v + i);\n  return (\n    <>\n      {list.map((v) => (\n        <Col sm={12} md={6} lg={3} key={v} className=\"mb-4 placeholder-glow\">\n          <div className=\"small mb-1 placeholder\" style={{ width: '100px' }} />\n          <div className=\"d-flex align-items-center\">\n            <div\n              style={{ width: '40px', height: '40px' }}\n              className=\"placeholder rounded flex-shrink-0\"\n            />\n            <div className=\"small ms-2\">\n              <div className=\"placeholder lh-1\" style={{ width: '80px' }} />\n              <div\n                className=\"text-secondary placeholder\"\n                style={{ width: '150px' }}\n              />\n            </div>\n          </div>\n          <div className=\"mt-1 d-block placeholder\" />\n          <div className=\"mt-1 d-block placeholder\" />\n        </Col>\n      ))}\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Badges/Detail/components/UserCard/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Avatar } from '@/components';\nimport { formatCount } from '@/utils';\n\ninterface Props {\n  data: any;\n}\n\nconst Index: FC<Props> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges' });\n  return (\n    <div className=\"d-flex\">\n      {data?.status !== 'deleted' ? (\n        <Link to={`/users/${data?.username}`}>\n          <Avatar\n            avatar={data?.avatar}\n            size=\"40px\"\n            className=\"me-2 d-none d-md-block\"\n            searchStr=\"s=96\"\n            alt={data?.display_name}\n          />\n\n          <Avatar\n            avatar={data?.avatar}\n            size=\"24px\"\n            className=\"me-2 d-block d-md-none\"\n            searchStr=\"s=48\"\n            alt={data?.display_name}\n          />\n        </Link>\n      ) : (\n        <>\n          <Avatar\n            avatar={data?.avatar}\n            size=\"40px\"\n            className=\"me-2 d-none d-md-block\"\n            searchStr=\"s=96\"\n            alt={data?.display_name}\n          />\n\n          <Avatar\n            avatar={data?.avatar}\n            size=\"24px\"\n            className=\"me-2 d-block d-md-none\"\n            searchStr=\"s=48\"\n            alt={data?.display_name}\n          />\n        </>\n      )}\n      <div className=\"small text-secondary d-flex flex-column\">\n        {data?.status !== 'deleted' ? (\n          <Link\n            to={`/users/${data?.username}`}\n            className=\"me-1 text-break name-ellipsis\"\n            style={{ maxWidth: '100px' }}>\n            {data?.display_name}\n          </Link>\n        ) : (\n          <span className=\"me-1 text-break\">{data?.display_name}</span>\n        )}\n        <div className=\"text-secondary\">\n          {formatCount(data?.rank)}{' '}\n          {t('x_reputation', { keyPrefix: 'personal' })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Badges/Detail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link, useParams, useSearchParams } from 'react-router-dom';\n\n// import classnames from 'classnames';\n\nimport { FormatTime, Pagination } from '@/components';\nimport { usePageTags, useSkeletonControl } from '@/hooks';\n// import { formatCount } from '@/utils';\nimport { useGetBadgeInfo, useBadgeDetailList } from '@/services';\n\nimport BadgeDetail from './components/Badge';\nimport Loader from './components/Loader';\nimport HeaderLoader from './components/HeaderLoader';\nimport UserCard from './components/UserCard';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges' });\n\n  const { badge_id = '' } = useParams();\n  const [urlSearchParams] = useSearchParams();\n\n  const page = Number(urlSearchParams.get('page')) || 1;\n  const pageSize = 30;\n  const { data: badgeInfo, isLoading: isHeaderLoading } =\n    useGetBadgeInfo(badge_id);\n  const { data: badges, isLoading: isDetailLoading } = useBadgeDetailList({\n    badge_id: badgeInfo?.id || '',\n    page,\n    page_size: pageSize,\n    username: urlSearchParams.get('username') || null,\n  });\n\n  const { isSkeletonShow } = useSkeletonControl(isDetailLoading);\n\n  usePageTags({\n    title: badgeInfo?.name || '',\n  });\n\n  if (badgeInfo === undefined) {\n    return null;\n  }\n\n  return (\n    <div className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      {isHeaderLoading ? <HeaderLoader /> : <BadgeDetail data={badgeInfo} />}\n      <Row>\n        {isSkeletonShow ? (\n          <Loader />\n        ) : (\n          badges?.list?.map((item, index) => {\n            const linkUrl =\n              item.object_type === 'question'\n                ? `/questions/${item.question_id}`\n                : item.object_type === 'answer'\n                  ? `/questions/${item.question_id}/${item.answer_id}`\n                  : item.object_type === 'comment' && item.answer_id\n                    ? `/questions/${item.question_id}/${item.answer_id}?commentId=${item.comment_id}`\n                    : item.object_type === 'comment'\n                      ? `/questions/${item.question_id}?commentId=${item.comment_id}`\n                      : '';\n            return (\n              <Col\n                sm={12}\n                md={6}\n                lg={3}\n                key={item.object_id || `${item.author_user_info.id}${index}`}\n                className=\"mb-4\">\n                <FormatTime\n                  time={item.created_at}\n                  preFix={t('awarded')}\n                  className=\"small mb-1 d-block\"\n                />\n                <UserCard data={item.author_user_info} />\n                {item.url_title && (\n                  <Link to={linkUrl} className=\"mt-1 d-block\">\n                    {item.url_title}\n                  </Link>\n                )}\n              </Col>\n            );\n          })\n        )}\n      </Row>\n      <div className=\"d-flex justify-content-center\">\n        <Pagination\n          currentPage={page}\n          pageSize={pageSize}\n          totalSize={badges?.count || 0}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Badges/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useTranslation } from 'react-i18next';\nimport { Row, Col } from 'react-bootstrap';\n\nimport { CardBadge } from '@/components';\nimport { usePageTags } from '@/hooks';\nimport { useGetAllBadges } from '@/services';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'badges' });\n\n  const { data: badgesList } = useGetAllBadges();\n\n  usePageTags({\n    title: t('title'),\n  });\n\n  return (\n    <div className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      {badgesList?.map((item) => {\n        return (\n          <div key={item.group_name} className=\"mb-4\">\n            <h5 className=\"mb-4\">{item.group_name}</h5>\n            <Row>\n              {item.badges?.map((badge) => {\n                return (\n                  <Col sm={6} md={4} lg={3} key={badge.id} className=\"mb-4\">\n                    <CardBadge data={badge} showAwardedCount />\n                  </Col>\n                );\n              })}\n            </Row>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/FifthStep/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\n\nimport Progress from '../Progress';\n\ninterface Props {\n  visible: boolean;\n  siteUrl: string;\n}\nconst Index: FC<Props> = ({ visible, siteUrl = '' }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n\n  if (!visible) return null;\n  return (\n    <div>\n      <h5>{t('ready_title')}</h5>\n      <p>\n        <Trans i18nKey=\"install.ready_desc\">\n          If you ever feel like changing more settings, visit\n          <a href={`${siteUrl}/users/login`}> admin section</a>; find it in the\n          site menu.\n        </Trans>\n      </p>\n      <p>{t('good_luck')}</p>\n\n      <div className=\"d-flex align-items-center justify-content-between\">\n        <Progress step={5} />\n        <Button href={siteUrl}>{t('done')}</Button>\n      </div>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/FirstStep/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport type { LangsType, FormValue, FormDataType } from '@/common/interface';\nimport Progress from '../Progress';\nimport { getInstallLangOptions } from '@/services';\nimport { setupInstallLanguage } from '@/utils/localize';\nimport { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';\nimport { Storage } from '@/utils';\n\ninterface Props {\n  data: FormValue;\n  changeCallback: (value: FormDataType) => void;\n  nextCallback: () => void;\n  visible: boolean;\n}\nconst Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n\n  const [langs, setLangs] = useState<LangsType[]>();\n\n  const getLangs = async () => {\n    const res: LangsType[] = await getInstallLangOptions();\n    const currentLang = Storage.get(CURRENT_LANG_STORAGE_KEY);\n    const selectedLang = currentLang || res[0].value;\n\n    setLangs(res);\n    setupInstallLanguage(selectedLang);\n\n    changeCallback({\n      lang: {\n        value: selectedLang,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const handleSubmit = () => {\n    nextCallback();\n  };\n\n  useEffect(() => {\n    getLangs();\n  }, []);\n\n  if (!visible) return null;\n  return (\n    <Form noValidate onSubmit={handleSubmit}>\n      <Form.Group controlId=\"lang\" className=\"mb-3\">\n        <Form.Label>{t('lang.label')}</Form.Label>\n        <Form.Select\n          value={data.value}\n          isInvalid={data.isInvalid}\n          onChange={(e) => {\n            setupInstallLanguage(e.target.value);\n            changeCallback({\n              lang: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}>\n          {langs?.map((item) => {\n            return (\n              <option value={item.value} key={item.value}>\n                {item.label}\n              </option>\n            );\n          })}\n        </Form.Select>\n      </Form.Group>\n\n      <div className=\"d-flex align-items-center justify-content-between\">\n        <Progress step={1} />\n        <Button type=\"submit\">{t('next')}</Button>\n      </div>\n    </Form>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/FourthStep/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, FormEvent } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport type { FormDataType } from '@/common/interface';\nimport Pattern from '@/common/pattern';\nimport Progress from '../Progress';\n\ninterface Props {\n  data: FormDataType;\n  changeCallback: (value: FormDataType) => void;\n  nextCallback: () => void;\n  visible: boolean;\n}\nconst Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const {\n      site_name,\n      site_url,\n      confirm_password,\n      contact_email,\n      name,\n      password,\n      email,\n    } = data;\n    const nameRegex = /^[\\w.-\\s]{2,30}$/;\n\n    if (!site_name.value) {\n      bol = false;\n      data.site_name = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('site_name.msg'),\n      };\n    }\n\n    if (site_name.value && site_name.value.length > 30) {\n      bol = false;\n      data.site_name = {\n        value: site_name.value,\n        isInvalid: true,\n        errorMsg: t('site_name.msg_max_length'),\n      };\n    }\n\n    if (!site_url.value) {\n      bol = false;\n      data.site_url = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('site_url.msg.empty'),\n      };\n    }\n\n    const reg = /^(http|https):\\/\\//g;\n    if (site_url.value && !site_url.value.match(reg)) {\n      bol = false;\n      data.site_url = {\n        value: site_url.value,\n        isInvalid: true,\n        errorMsg: t('site_url.msg.incorrect'),\n      };\n    } else if (site_url.value.length > 512) {\n      bol = false;\n      data.site_url = {\n        value: site_url.value,\n        isInvalid: true,\n        errorMsg: t('site_url.msg.max_length'),\n      };\n    }\n\n    if (!contact_email.value) {\n      bol = false;\n      data.contact_email = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('contact_email.msg.empty'),\n      };\n    }\n\n    if (contact_email.value && !Pattern.email.test(contact_email.value)) {\n      bol = false;\n      data.contact_email = {\n        value: contact_email.value,\n        isInvalid: true,\n        errorMsg: t('contact_email.msg.incorrect'),\n      };\n    }\n\n    if (!name.value) {\n      bol = false;\n      data.name = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('admin_name.msg'),\n      };\n    } else if (name.value.length < 2 || name.value.length > 30) {\n      bol = false;\n      data.name = {\n        value: name.value,\n        isInvalid: true,\n        errorMsg: t('admin_name.msg_max_length'),\n      };\n    } else if (!nameRegex.test(name.value)) {\n      bol = false;\n      data.name = {\n        value: name.value,\n        isInvalid: true,\n        errorMsg: t('admin_name.character'),\n      };\n    }\n\n    if (!password.value) {\n      bol = false;\n      data.password = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('admin_password.msg'),\n      };\n    }\n\n    if (password.value && password.value.length < 4) {\n      bol = false;\n      data.password = {\n        value: data.password.value,\n        isInvalid: true,\n        errorMsg: t('admin_password.msg_min_length'),\n      };\n    }\n\n    if (password.value && password.value.length > 32) {\n      bol = false;\n      data.password = {\n        value: data.password.value,\n        isInvalid: true,\n        errorMsg: t('admin_password.msg_max_length'),\n      };\n    }\n\n    if (confirm_password.value !== password.value) {\n      bol = false;\n      data.confirm_password = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('admin_confirm_password.msg'),\n      };\n    }\n\n    if (!email.value) {\n      bol = false;\n      data.email = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('admin_email.msg.empty'),\n      };\n    }\n\n    if (email.value && !Pattern.email.test(email.value)) {\n      bol = false;\n      data.email = {\n        value: email.value,\n        isInvalid: true,\n        errorMsg: t('admin_email.msg.incorrect'),\n      };\n    }\n\n    changeCallback({\n      ...data,\n    });\n    return bol;\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    nextCallback();\n  };\n\n  if (!visible) return null;\n  return (\n    <Form noValidate onSubmit={handleSubmit}>\n      <h5>{t('site_information')}</h5>\n      <Form.Group controlId=\"site_name\" className=\"mb-3\">\n        <Form.Label>{t('site_name.label')}</Form.Label>\n        <Form.Control\n          required\n          value={data.site_name.value}\n          isInvalid={data.site_name.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              site_name: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Control.Feedback type=\"invalid\">\n          {data.site_name.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n      <Form.Group controlId=\"site_url\" className=\"mb-3\">\n        <Form.Label>{t('site_url.label')}</Form.Label>\n        <Form.Control\n          required\n          value={data.site_url.value}\n          isInvalid={data.site_url.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              site_url: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('site_url.text')}</Form.Text>\n        <Form.Control.Feedback type=\"invalid\">\n          {data.site_url.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n      <Form.Group controlId=\"contact_email\" className=\"mb-3\">\n        <Form.Label>{t('contact_email.label')}</Form.Label>\n        <Form.Control\n          required\n          type=\"email\"\n          value={data.contact_email.value}\n          isInvalid={data.contact_email.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              contact_email: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('contact_email.text')}</Form.Text>\n        <Form.Control.Feedback type=\"invalid\">\n          {data.contact_email.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n      <h5>{t('login_required.label')}</h5>\n      <Form.Group controlId=\"login_required\" className=\"mb-3\">\n        <Form.Label>{t('login_required.label')}</Form.Label>\n        <Form.Check\n          type=\"switch\"\n          id=\"login_required\"\n          label={t('login_required.switch')}\n          checked={data.login_required.value}\n          onChange={(e) => {\n            changeCallback({\n              login_required: {\n                value: e.target.checked,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('login_required.text')}</Form.Text>\n      </Form.Group>\n      <Form.Group controlId=\"external_content_display\" className=\"mb-3\">\n        <Form.Label>\n          {t('external_content_display.label', { keyPrefix: 'admin.legal' })}\n        </Form.Label>\n        <Form.Select\n          value={data.external_content_display.value}\n          onChange={(e) => {\n            changeCallback({\n              external_content_display: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}>\n          <option value=\"always_display\">\n            {t('external_content_display.always_display', {\n              keyPrefix: 'admin.legal',\n            })}\n          </option>\n          <option value=\"ask_before_display\">\n            {t('external_content_display.ask_before_display', {\n              keyPrefix: 'admin.legal',\n            })}\n          </option>\n        </Form.Select>\n        <Form.Text>\n          {t('external_content_display.text', { keyPrefix: 'admin.legal' })}\n        </Form.Text>\n      </Form.Group>\n\n      <h5>{t('admin_account')}</h5>\n      <Form.Group controlId=\"name\" className=\"mb-3\">\n        <Form.Label>{t('admin_name.label')}</Form.Label>\n        <Form.Control\n          required\n          value={data.name.value}\n          isInvalid={data.name.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              name: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Control.Feedback type=\"invalid\">\n          {data.name.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <Form.Group controlId=\"password\" className=\"mb-3\">\n        <Form.Label>{t('admin_password.label')}</Form.Label>\n        <Form.Control\n          required\n          type=\"password\"\n          value={data.password.value}\n          isInvalid={data.password.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              password: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('admin_password.text')}</Form.Text>\n        <Form.Control.Feedback type=\"invalid\">\n          {data.password.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <Form.Group controlId=\"confirm_password\" className=\"mb-3\">\n        <Form.Label>{t('admin_confirm_password.label')}</Form.Label>\n        <Form.Control\n          required\n          type=\"password\"\n          value={data.confirm_password.value}\n          isInvalid={data.confirm_password.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              confirm_password: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('admin_confirm_password.text')}</Form.Text>\n        <Form.Control.Feedback type=\"invalid\">\n          {data.confirm_password.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <Form.Group controlId=\"email\" className=\"mb-3\">\n        <Form.Label>{t('admin_email.label')}</Form.Label>\n        <Form.Control\n          required\n          value={data.email.value}\n          isInvalid={data.email.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              email: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Text>{t('admin_email.text')}</Form.Text>\n        <Form.Control.Feedback type=\"invalid\">\n          {data.email.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <div className=\"d-flex align-items-center justify-content-between\">\n        <Progress step={4} />\n        <Button type=\"submit\">{t('next')}</Button>\n      </div>\n    </Form>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/Progress/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ProgressBar } from 'react-bootstrap';\n\ninterface IProps {\n  step: number;\n}\n\nconst Index: FC<IProps> = ({ step }) => {\n  return (\n    <div className=\"d-flex align-items-center small text-secondary\">\n      <ProgressBar\n        now={(step / 5) * 100}\n        variant=\"success\"\n        style={{ width: '200px' }}\n        className=\"me-2\"\n      />\n      <span>{step}/5</span>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Install/components/SecondStep/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, FormEvent } from 'react';\nimport { Form, Button, Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport Progress from '../Progress';\nimport type { FormDataType } from '@/common/interface';\n\ninterface Props {\n  data: FormDataType;\n  changeCallback: (value: FormDataType) => void;\n  nextCallback: () => void;\n  visible: boolean;\n}\n\nconst sqlData = [\n  {\n    value: 'mysql',\n    label: 'MariaDB/MySQL',\n  },\n  {\n    value: 'sqlite3',\n    label: 'SQLite',\n  },\n  {\n    value: 'postgres',\n    label: 'PostgreSQL',\n  },\n];\n\nconst sslModes = [\n  {\n    value: 'require',\n  },\n  {\n    value: 'verify-ca',\n  },\n  {\n    value: 'verify-full',\n  },\n];\n\nconst Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { db_type, db_username, db_password, db_host, db_name, db_file } =\n      data;\n\n    if (db_type.value !== 'sqlite3') {\n      if (!db_username.value) {\n        bol = false;\n        data.db_username = {\n          value: '',\n          isInvalid: true,\n          errorMsg: t('db_username.msg'),\n        };\n      }\n      if (!db_password.value) {\n        bol = false;\n        data.db_password = {\n          value: '',\n          isInvalid: true,\n          errorMsg: t('db_password.msg'),\n        };\n      }\n\n      if (!db_host.value) {\n        bol = false;\n        data.db_host = {\n          value: '',\n          isInvalid: true,\n          errorMsg: t('db_host.msg'),\n        };\n      }\n      if (!db_name.value) {\n        bol = false;\n        data.db_name = {\n          value: '',\n          isInvalid: true,\n          errorMsg: t('db_name.msg'),\n        };\n      }\n    } else if (!db_file.value) {\n      bol = false;\n      data.db_file = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('db_file.msg'),\n      };\n    }\n    changeCallback({\n      ...data,\n    });\n    return bol;\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    nextCallback();\n  };\n\n  if (!visible) return null;\n  return (\n    <Form noValidate onSubmit={handleSubmit}>\n      <Form.Group controlId=\"database_engine\" className=\"mb-3\">\n        <Form.Label>{t('db_type.label')}</Form.Label>\n        <Form.Select\n          value={data.db_type.value}\n          isInvalid={data.db_type.isInvalid}\n          onChange={(e) => {\n            changeCallback({\n              db_type: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}>\n          {sqlData.map((item) => {\n            return (\n              <option key={item.value} value={item.value}>\n                {item.label}\n              </option>\n            );\n          })}\n        </Form.Select>\n      </Form.Group>\n      {data.db_type.value !== 'sqlite3' ? (\n        <>\n          <Form.Group controlId=\"username\" className=\"mb-3\">\n            <Form.Label>{t('db_username.label')}</Form.Label>\n            <Form.Control\n              required\n              placeholder={t('db_username.placeholder')}\n              value={data.db_username.value}\n              isInvalid={data.db_username.isInvalid}\n              onChange={(e) => {\n                changeCallback({\n                  db_username: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                });\n              }}\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {data.db_username.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"db_password\" className=\"mb-3\">\n            <Form.Label>{t('db_password.label')}</Form.Label>\n            <Form.Control\n              required\n              value={data.db_password.value}\n              isInvalid={data.db_password.isInvalid}\n              onChange={(e) => {\n                changeCallback({\n                  db_password: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                });\n              }}\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {data.db_password.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n          {data.db_type.value === 'postgres' && (\n            <Form.Group controlId=\"ssl_enabled\" className=\"mb-3\">\n              <Form.Label>{t('ssl_enabled.label')}</Form.Label>\n              <Form.Check\n                type=\"switch\"\n                label={`${\n                  data.ssl_enabled.value\n                    ? t('ssl_enabled_on.label')\n                    : t('ssl_enabled_off.label')\n                }`}\n                checked={data.ssl_enabled.value}\n                onChange={(e) => {\n                  changeCallback({\n                    ssl_enabled: {\n                      value: e.target.checked,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                    ssl_mode: {\n                      value: 'require',\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                    ssl_root_cert: {\n                      value: '',\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                    ssl_cert: {\n                      value: '',\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                    ssl_key: {\n                      value: '',\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}\n              />\n            </Form.Group>\n          )}\n          {data.db_type.value === 'postgres' && data.ssl_enabled.value && (\n            <Form.Group controlId=\"sslmodeOptionsDropdown\" className=\"mb-3\">\n              <Form.Label>{t('ssl_mode.label')}</Form.Label>\n              <Form.Select\n                value={data.ssl_mode.value}\n                onChange={(e) => {\n                  changeCallback({\n                    ssl_mode: {\n                      value: e.target.value,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}>\n                {sslModes.map((item) => {\n                  return (\n                    <option value={item.value} key={item.value}>\n                      {item.value}\n                    </option>\n                  );\n                })}\n              </Form.Select>\n            </Form.Group>\n          )}\n          {data.db_type.value === 'postgres' &&\n            data.ssl_enabled.value &&\n            (data.ssl_mode.value === 'verify-ca' ||\n              data.ssl_mode.value === 'verify-full') && (\n              <Row className=\"mb-3\">\n                <Form.Group as={Col} controlId=\"ssl_root_cert\">\n                  <Form.Control\n                    placeholder={t('ssl_root_cert.placeholder')}\n                    aria-label=\"ssl_root_cert\"\n                    aria-describedby=\"basic-addon1\"\n                    isInvalid={data.ssl_root_cert.isInvalid}\n                    onChange={(e) => {\n                      changeCallback({\n                        ssl_root_cert: {\n                          value: e.target.value,\n                          isInvalid: false,\n                          errorMsg: '',\n                        },\n                      });\n                    }}\n                    required\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {`${data.ssl_root_cert.errorMsg}`}\n                  </Form.Control.Feedback>\n                </Form.Group>\n                <Form.Group as={Col} controlId=\"ssl_cert\">\n                  <Form.Control\n                    placeholder={t('ssl_cert.placeholder')}\n                    aria-label=\"ssl_cert\"\n                    aria-describedby=\"basic-addon1\"\n                    isInvalid={data.ssl_cert.isInvalid}\n                    onChange={(e) => {\n                      changeCallback({\n                        ssl_cert: {\n                          value: e.target.value,\n                          isInvalid: false,\n                          errorMsg: '',\n                        },\n                      });\n                    }}\n                    required\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {`${data.ssl_cert.errorMsg}`}\n                  </Form.Control.Feedback>\n                </Form.Group>\n                <Form.Group as={Col} controlId=\"ssl_key\">\n                  <Form.Control\n                    placeholder={t('ssl_key.placeholder')}\n                    aria-label=\"ssl_key\"\n                    aria-describedby=\"basic-addon1\"\n                    isInvalid={data.ssl_key.isInvalid}\n                    onChange={(e) => {\n                      changeCallback({\n                        ssl_key: {\n                          value: e.target.value,\n                          isInvalid: false,\n                          errorMsg: '',\n                        },\n                      });\n                    }}\n                    required\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {`${data.ssl_key.errorMsg}`}\n                  </Form.Control.Feedback>\n                </Form.Group>\n              </Row>\n            )}\n          <Form.Group controlId=\"db_host\" className=\"mb-3\">\n            <Form.Label>{t('db_host.label')}</Form.Label>\n            <Form.Control\n              required\n              placeholder={t('db_host.placeholder')}\n              value={data.db_host.value}\n              isInvalid={data.db_host.isInvalid}\n              onChange={(e) => {\n                changeCallback({\n                  db_host: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                });\n              }}\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {data.db_host.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n          <Form.Group controlId=\"name\" className=\"mb-3\">\n            <Form.Label>{t('db_name.label')}</Form.Label>\n            <Form.Control\n              required\n              placeholder={t('db_name.placeholder')}\n              value={data.db_name.value}\n              isInvalid={data.db_name.isInvalid}\n              onChange={(e) => {\n                changeCallback({\n                  db_name: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                });\n              }}\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {data.db_name.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n        </>\n      ) : (\n        <Form.Group controlId=\"file\" className=\"mb-3\">\n          <Form.Label>{t('db_file.label')}</Form.Label>\n          <Form.Control\n            required\n            placeholder={t('db_file.placeholder')}\n            value={data.db_file.value}\n            isInvalid={data.db_file.isInvalid}\n            onChange={(e) => {\n              changeCallback({\n                db_file: {\n                  value: e.target.value,\n                  isInvalid: false,\n                  errorMsg: '',\n                },\n              });\n            }}\n          />\n          <Form.Control.Feedback type=\"invalid\">\n            {data.db_file.errorMsg}\n          </Form.Control.Feedback>\n        </Form.Group>\n      )}\n\n      <div className=\"d-flex align-items-center justify-content-between\">\n        <Progress step={2} />\n        <Button type=\"submit\">{t('next')}</Button>\n      </div>\n    </Form>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/ThirdStep/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Form, Button, FormGroup } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\n\nimport Progress from '../Progress';\n\ninterface Props {\n  visible: boolean;\n  errorMsg;\n  nextCallback: () => void;\n}\n\nconst Index: FC<Props> = ({ visible, errorMsg, nextCallback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n\n  if (!visible) return null;\n  return (\n    <div>\n      <h5>{t('config_yaml.title')}</h5>\n\n      {errorMsg?.msg?.length > 0 ? (\n        <>\n          <div className=\"fmt\">\n            <p>\n              <Trans\n                i18nKey=\"install.config_yaml.desc\"\n                components={{ 1: <code /> }}\n              />\n            </p>\n          </div>\n          <FormGroup className=\"mb-3\">\n            <Form.Control\n              type=\"text\"\n              as=\"textarea\"\n              rows={8}\n              className=\"small\"\n              value={errorMsg?.default_config}\n            />\n          </FormGroup>\n          <div className=\"mb-3\">{t('config_yaml.info')}</div>\n        </>\n      ) : (\n        <div className=\"mb-3\">{t('config_yaml.label')}</div>\n      )}\n\n      <div className=\"d-flex align-items-center justify-content-between\">\n        <Progress step={3} />\n        <Button onClick={nextCallback}>{t('next')}</Button>\n      </div>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Install/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport FirstStep from './FirstStep';\nimport SecondStep from './SecondStep';\nimport ThirdStep from './ThirdStep';\nimport FourthStep from './FourthStep';\nimport Fifth from './FifthStep';\n\nexport { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth };\n"
  },
  {
    "path": "ui/src/pages/Install/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect } from 'react';\nimport { Container, Row, Col, Card, Alert } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\nimport { Helmet, HelmetProvider } from 'react-helmet-async';\n\nimport type { FormDataType } from '@/common/interface';\nimport {\n  dbCheck,\n  installInit,\n  installBaseInfo,\n  checkConfigFileExists,\n} from '@/services';\nimport {\n  Storage,\n  handleFormError,\n  scrollToDocTop,\n  scrollToElementTop,\n} from '@/utils';\nimport { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';\nimport { BASE_ORIGIN } from '@/router/alias';\nimport { setupInstallLanguage } from '@/utils/localize';\nimport { loggedUserInfoStore } from '@/stores';\n\nimport {\n  FirstStep,\n  SecondStep,\n  ThirdStep,\n  FourthStep,\n  Fifth,\n} from './components';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'install' });\n  const [step, setStep] = useState(1);\n  const [loading, setLoading] = useState(true);\n  const [errorData, setErrorData] = useState<{ [propName: string]: any }>({\n    msg: '',\n  });\n  const [checkData, setCheckData] = useState({\n    db_table_exist: false,\n    db_connection_success: false,\n  });\n\n  const [formData, setFormData] = useState<FormDataType>({\n    lang: {\n      value: 'en_US',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_type: {\n      value: 'mysql',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_username: {\n      value: 'root',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_password: {\n      value: 'root',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_host: {\n      value: 'db:3306',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_name: {\n      value: 'answer',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    db_file: {\n      value: '/data/answer.db',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    site_name: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    site_url: {\n      value: BASE_ORIGIN,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    contact_email: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    login_required: {\n      value: false,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    external_content_display: {\n      value: 'always_display',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    name: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    password: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    confirm_password: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    email: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    ssl_enabled: {\n      value: false,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    ssl_mode: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    ssl_key: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    ssl_root_cert: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    ssl_cert: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const { update, user } = loggedUserInfoStore();\n\n  const updateFormData = (params: FormDataType) => {\n    if (Object.keys(params)?.[0] === 'db_type') {\n      let updatedFormData = formData;\n      if (params.db_type.value === 'mysql') {\n        updatedFormData = {\n          ...updatedFormData,\n          db_username: { ...updatedFormData.db_username, value: 'root' },\n          db_password: { ...updatedFormData.db_password, value: 'root' },\n          db_host: { ...updatedFormData.db_host, value: 'db:3306' },\n        };\n      } else if (params.db_type.value === 'postgres') {\n        updatedFormData = {\n          ...updatedFormData,\n          db_username: { ...updatedFormData.db_username, value: 'postgres' },\n          db_password: { ...updatedFormData.db_password, value: 'postgres' },\n          db_host: { ...updatedFormData.db_host, value: 'db:5432' },\n          ssl_enabled: { ...updatedFormData.ssl_enabled, value: false },\n          ssl_mode: { ...updatedFormData.ssl_mode, value: '' },\n        };\n      }\n      return updatedFormData;\n    }\n    return formData;\n  };\n\n  const handleChange = (params: FormDataType) => {\n    setErrorData({\n      msg: '',\n    });\n    const updatedFormData = updateFormData(params);\n    setFormData({ ...updatedFormData, ...params });\n  };\n\n  const handleErr = (data) => {\n    scrollToDocTop();\n    setErrorData(data);\n  };\n\n  const handleNext = async () => {\n    setErrorData({\n      msg: '',\n    });\n    setStep((pre) => pre + 1);\n  };\n\n  const checkInstall = () => {\n    const params = {\n      lang: formData.lang.value,\n      db_type: formData.db_type.value,\n      db_username: formData.db_username.value,\n      db_password: formData.db_password.value,\n      db_host: formData.db_host.value,\n      db_name: formData.db_name.value,\n      db_file: formData.db_file.value,\n      ssl_enabled: formData.ssl_enabled.value,\n      ssl_mode: formData.ssl_mode.value,\n      ssl_key: formData.ssl_key.value,\n      ssl_root_cert: formData.ssl_root_cert.value,\n      ssl_cert: formData.ssl_cert.value,\n    };\n    installInit(params)\n      .then(() => {\n        handleNext();\n      })\n      .catch((err) => {\n        handleErr(err);\n      });\n  };\n\n  const submitDatabaseForm = () => {\n    const params = {\n      lang: formData.lang.value,\n      db_type: formData.db_type.value,\n      db_username: formData.db_username.value,\n      db_password: formData.db_password.value,\n      db_host: formData.db_host.value,\n      db_name: formData.db_name.value,\n      db_file: formData.db_file.value,\n      ssl_enabled: formData.ssl_enabled.value,\n      ssl_mode: formData.ssl_mode.value,\n      ssl_key: formData.ssl_key.value,\n      ssl_root_cert: formData.ssl_root_cert.value,\n      ssl_cert: formData.ssl_cert.value,\n    };\n    dbCheck(params)\n      .then(() => {\n        checkInstall();\n      })\n      .catch((err) => {\n        handleErr(err);\n      });\n  };\n\n  const submitSiteConfig = () => {\n    const params = {\n      lang: formData.lang.value,\n      site_name: formData.site_name.value,\n      site_url: formData.site_url.value,\n      contact_email: formData.contact_email.value,\n      login_required: formData.login_required.value,\n      external_content_display: formData.external_content_display.value,\n      name: formData.name.value,\n      password: formData.password.value,\n      email: formData.email.value,\n    };\n\n    installBaseInfo(params)\n      .then(() => {\n        handleNext();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        } else {\n          handleErr(err);\n        }\n      });\n  };\n\n  const handleStep = () => {\n    if (step === 1) {\n      Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value);\n      update({\n        ...user,\n        language: formData.lang.value,\n      });\n      handleNext();\n    }\n    if (step === 2) {\n      submitDatabaseForm();\n    }\n    if (step === 3) {\n      if (errorData.msg) {\n        checkInstall();\n      } else {\n        handleNext();\n      }\n    }\n    if (step === 4) {\n      submitSiteConfig();\n    }\n    if (step > 4) {\n      handleNext();\n    }\n  };\n\n  const handleInstallNow = (e) => {\n    e.preventDefault();\n    if (checkData.db_table_exist) {\n      setStep(8);\n    } else {\n      setStep(4);\n    }\n  };\n\n  const configYmlCheck = () => {\n    checkConfigFileExists()\n      .then((res) => {\n        setCheckData({\n          db_table_exist: res.db_table_exist,\n          db_connection_success: res.db_connection_success,\n        });\n        if (res && res.config_file_exist) {\n          setupInstallLanguage(Storage.get(CURRENT_LANG_STORAGE_KEY));\n          if (res.db_connection_success) {\n            setStep(6);\n          } else {\n            setStep(7);\n          }\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n\n  useEffect(() => {\n    configYmlCheck();\n  }, []);\n\n  if (loading) {\n    return <div />;\n  }\n\n  return (\n    <HelmetProvider>\n      <Helmet>\n        <title>{t('install', { keyPrefix: 'page_title' })}</title>\n      </Helmet>\n      <div className=\"bg-f5 py-5 flex-grow-1\">\n        <Container className=\"py-3\">\n          <Row className=\"justify-content-center\">\n            <Col lg={6}>\n              <h2 className=\"mb-4 text-center\">{t('title')}</h2>\n              <Card>\n                <Card.Body>\n                  {errorData?.msg && (\n                    <Alert variant=\"danger\">{errorData?.msg}</Alert>\n                  )}\n\n                  <FirstStep\n                    visible={step === 1}\n                    data={formData.lang}\n                    changeCallback={handleChange}\n                    nextCallback={handleStep}\n                  />\n\n                  <SecondStep\n                    visible={step === 2}\n                    data={formData}\n                    changeCallback={handleChange}\n                    nextCallback={handleStep}\n                  />\n\n                  <ThirdStep\n                    visible={step === 3}\n                    nextCallback={handleStep}\n                    errorMsg={errorData}\n                  />\n\n                  <FourthStep\n                    visible={step === 4}\n                    data={formData}\n                    changeCallback={handleChange}\n                    nextCallback={handleStep}\n                  />\n\n                  <Fifth\n                    visible={step === 5}\n                    siteUrl={formData.site_url.value}\n                  />\n                  {step === 6 && (\n                    <div>\n                      <h5>{t('warn_title')}</h5>\n                      <p>\n                        <Trans\n                          i18nKey=\"install.warn_desc\"\n                          components={{ 1: <code /> }}\n                        />{' '}\n                        <Trans i18nKey=\"install.install_now\">\n                          You may try\n                          <a href=\"###\" onClick={(e) => handleInstallNow(e)}>\n                            installing now\n                          </a>\n                          .\n                        </Trans>\n                      </p>\n                    </div>\n                  )}\n\n                  {step === 7 && (\n                    <div>\n                      <h5>{t('db_failed')}</h5>\n                      <p>\n                        <Trans\n                          i18nKey=\"install.db_failed_desc\"\n                          components={{ 1: <code /> }}\n                        />\n                      </p>\n                    </div>\n                  )}\n\n                  {step === 8 && (\n                    <div>\n                      <h5>{t('installed')}</h5>\n                      <p>{t('installed_desc')}</p>\n                    </div>\n                  )}\n                </Card.Body>\n              </Card>\n            </Col>\n          </Row>\n        </Container>\n      </div>\n    </HelmetProvider>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Layout/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect } from 'react';\nimport { Outlet, useLocation, ScrollRestoration } from 'react-router-dom';\nimport { HelmetProvider } from 'react-helmet-async';\n\nimport { SWRConfig } from 'swr';\nimport classnames from 'classnames';\n\nimport {\n  toastStore,\n  loginToContinueStore,\n  errorCodeStore,\n  siteSecurityStore,\n  themeSettingStore,\n} from '@/stores';\nimport {\n  Header,\n  Toast,\n  Customize,\n  CustomizeTheme,\n  PageTags,\n  HttpErrorContent,\n} from '@/components';\nimport { LoginToContinueModal, BadgeModal } from '@/components/Modal';\nimport { changeTheme, Storage, scrollToElementTop } from '@/utils';\nimport { useQueryNotificationStatus } from '@/services';\nimport { useExternalToast } from '@/hooks';\nimport { EXTERNAL_CONTENT_DISPLAY_MODE } from '@/common/constants';\n\nconst Layout: FC = () => {\n  const location = useLocation();\n  const { msg: toastMsg, variant, clear: toastClear } = toastStore();\n  const externalToast = useExternalToast();\n  const externalContentDisplay = siteSecurityStore(\n    (state) => state.external_content_display,\n  );\n  const closeToast = () => {\n    toastClear();\n  };\n  const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore();\n  const { show: showLoginToContinueModal } = loginToContinueStore();\n  const { data: notificationData } = useQueryNotificationStatus();\n  const layout = themeSettingStore((state) => state.layout);\n  useEffect(() => {\n    // handle footnote links\n    const fixFootnoteLinks = () => {\n      const footnoteLinks = document.querySelectorAll(\n        'a[href^=\"#\"]:not([data-footnote-fixed])',\n      );\n\n      footnoteLinks.forEach((link) => {\n        link.setAttribute('data-footnote-fixed', 'true');\n        const href = link.getAttribute('href');\n        link.addEventListener('click', (e) => {\n          e.preventDefault();\n          const targetId = href?.substring(1) || '';\n          const targetElement = document.getElementById(targetId);\n\n          if (targetElement) {\n            window.history.pushState(null, '', `${location.pathname}${href}`);\n\n            scrollToElementTop(targetElement);\n          }\n        });\n      });\n\n      if (window.location.hash) {\n        const { hash } = window.location;\n        const targetElement = document.getElementById(hash.substring(1));\n\n        if (targetElement) {\n          setTimeout(() => {\n            scrollToElementTop(targetElement);\n          }, 100);\n        }\n      }\n    };\n    fixFootnoteLinks();\n\n    const observer = new MutationObserver(() => {\n      fixFootnoteLinks();\n    });\n\n    observer.observe(document.body, {\n      childList: true,\n      subtree: true,\n      attributes: true,\n      attributeFilter: ['id', 'href'],\n    });\n\n    const handleHashChange = () => {\n      if (window.location.hash) {\n        const { hash } = window.location;\n        const targetElement = document.getElementById(hash.substring(1));\n\n        if (targetElement) {\n          setTimeout(() => {\n            scrollToElementTop(targetElement);\n          }, 100);\n        }\n      }\n    };\n\n    window.addEventListener('hashchange', handleHashChange);\n\n    return () => {\n      observer.disconnect();\n      window.removeEventListener('hashchange', handleHashChange);\n    };\n  }, [location.pathname]);\n\n  useEffect(() => {\n    httpStatusReset();\n  }, [location]);\n\n  useEffect(() => {\n    const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    function handleSystemThemeChange(event) {\n      if (event.matches) {\n        changeTheme('dark');\n      } else {\n        changeTheme('light');\n      }\n    }\n\n    systemThemeQuery.addListener(handleSystemThemeChange);\n\n    return () => {\n      systemThemeQuery.removeListener(handleSystemThemeChange);\n    };\n  }, []);\n\n  const replaceImgSrc = () => {\n    const storageUserExternalMode = Storage.get(EXTERNAL_CONTENT_DISPLAY_MODE);\n    const images = document.querySelectorAll(\n      'img:not([data-processed])',\n    ) as NodeListOf<HTMLImageElement>;\n\n    images.forEach((img) => {\n      // Mark as processed to avoid duplication\n      img.setAttribute('data-processed', 'true');\n\n      if (\n        img.src &&\n        storageUserExternalMode !== 'always' &&\n        !img.src.startsWith('/') &&\n        !img.src.startsWith('data:') &&\n        !img.src.startsWith('blob:') &&\n        !img.src.startsWith(window.location.origin)\n      ) {\n        externalToast.onShow();\n        img.dataset.src = img.src;\n        img.removeAttribute('src');\n      }\n    });\n  };\n\n  useEffect(() => {\n    // Controlling the loading of external image resources\n    const observer = new MutationObserver((mutationsList) => {\n      let hasNewImages = false;\n      mutationsList.forEach((mutation) => {\n        if (mutation.type === 'childList') {\n          mutation.addedNodes.forEach((node) => {\n            if (\n              node.nodeName === 'IMG' ||\n              (node.nodeType === 1 &&\n                (node as Element).querySelectorAll('img:not([data-processed])')\n                  .length > 0)\n            ) {\n              hasNewImages = true;\n            }\n          });\n        }\n      });\n      if (hasNewImages) {\n        replaceImgSrc();\n      }\n    });\n\n    if (externalContentDisplay !== 'always_display') {\n      observer.observe(document.body, { childList: true, subtree: true });\n    }\n\n    return () => observer.disconnect();\n  }, [externalContentDisplay]);\n  return (\n    <HelmetProvider>\n      <PageTags />\n      <CustomizeTheme />\n      <SWRConfig\n        value={{\n          revalidateOnFocus: false,\n        }}>\n        <Header />\n        <div\n          className={classnames(\n            'position-relative page-wrap d-flex flex-column flex-fill',\n            layout === 'Fixed-width' ? 'container-xxl' : '',\n          )}>\n          {httpStatusCode ? (\n            <HttpErrorContent httpCode={httpStatusCode} />\n          ) : (\n            <Outlet />\n          )}\n        </div>\n        <Toast msg={toastMsg} variant={variant} onClose={closeToast} />\n        <Customize />\n        <LoginToContinueModal visible={showLoginToContinueModal} />\n        <BadgeModal\n          badge={notificationData?.badge_award}\n          visible={Boolean(notificationData?.badge_award)}\n        />\n        <ScrollRestoration />\n      </SWRConfig>\n    </HelmetProvider>\n  );\n};\n\nexport default memo(Layout);\n"
  },
  {
    "path": "ui/src/pages/Legal/Privacy/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { useLegalPrivacy } from '@/services';\nimport { htmlRender } from '@/components';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });\n  usePageTags({\n    title: t('privacy'),\n  });\n  const { data: privacy } = useLegalPrivacy();\n  const contentText = privacy?.privacy_policy_original_text;\n  let matchUrl: URL | undefined;\n\n  useEffect(() => {\n    const fmt = document.querySelector('.fmt') as HTMLElement;\n    if (!fmt) {\n      return;\n    }\n    htmlRender(fmt, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [privacy?.privacy_policy_parsed_text]);\n\n  try {\n    if (contentText) {\n      matchUrl = new URL(contentText);\n    }\n    // eslint-disable-next-line no-empty\n  } catch (ex) {}\n  if (matchUrl) {\n    window.location.replace(matchUrl.toString());\n    return null;\n  }\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('privacy')}</h3>\n      <div\n        className=\"fmt\"\n        dangerouslySetInnerHTML={{\n          __html: privacy?.privacy_policy_parsed_text || '',\n        }}\n      />\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Legal/Tos/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { useLegalTos } from '@/services';\nimport { htmlRender } from '@/components';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });\n  usePageTags({\n    title: t('tos'),\n  });\n  const { data: tos } = useLegalTos();\n  const contentText = tos?.terms_of_service_original_text;\n  let matchUrl: URL | undefined;\n\n  useEffect(() => {\n    const fmt = document.querySelector('.fmt') as HTMLElement;\n    if (!fmt) {\n      return;\n    }\n    htmlRender(fmt, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [tos?.terms_of_service_parsed_text]);\n\n  try {\n    if (contentText) {\n      matchUrl = new URL(contentText);\n    }\n    // eslint-disable-next-line no-empty\n  } catch (ex) {}\n  if (matchUrl) {\n    window.location.replace(matchUrl.toString());\n    return null;\n  }\n\n  return (\n    <div>\n      <h3 className=\"mb-4\">{t('tos')}</h3>\n      <div\n        className=\"fmt\"\n        dangerouslySetInnerHTML={{\n          __html: tos?.terms_of_service_parsed_text || '',\n        }}\n      />\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Legal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Row, Col, Nav } from 'react-bootstrap';\nimport { Outlet, NavLink } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col xxl={12}>\n        <Nav\n          className=\"mb-4 flex-nowrap\"\n          variant=\"pills\"\n          style={{ overflow: 'auto' }}>\n          <NavLink to=\"/tos\" key=\"tos\" className=\"nav-link\">\n            {t('tos')}\n          </NavLink>\n          <NavLink to=\"/privacy\" key=\"privacy\" className=\"nav-link\">\n            {t('privacy')}\n          </NavLink>\n        </Nav>\n      </Col>\n      <Col xxl={12}>\n        <Outlet />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Maintenance/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Container } from 'react-bootstrap';\nimport { Helmet, HelmetProvider } from 'react-helmet-async';\nimport { useTranslation } from 'react-i18next';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'page_maintenance',\n  });\n  return (\n    <HelmetProvider>\n      <Helmet>\n        <title>{t('maintenance', { keyPrefix: 'page_title' })}</title>\n      </Helmet>\n      <div className=\"bg-f5\">\n        <Container\n          className=\"d-flex flex-column justify-content-center align-items-center\"\n          style={{ minHeight: '100vh' }}>\n          <div\n            className=\"mb-4 text-secondary\"\n            style={{ fontSize: '120px', lineHeight: 1.2 }}>\n            (=‘_‘=)\n          </div>\n          <div className=\"text-center mb-4\">{t('desc')}</div>\n        </Container>\n      </div>\n    </HelmetProvider>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Questions/Ask/components/SearchQuestion/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.search-question-wrap {\n  .accordion-button {\n    &::after {\n      background-image: var(--bs-accordion-btn-icon);\n    }\n  }\n  .list-group-item {\n    &.active {\n      background-color: #e9ecef;\n      border-color: rgba(0, 0, 0, 0.0625);\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { Accordion, ListGroup } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { Icon } from '@/components';\nimport { pathFactory } from '@/router/pathFactory';\n\nimport './index.scss';\n\nconst SearchQuestion = ({ similarQuestions }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ask' });\n  // set max similar number\n  if (similarQuestions && similarQuestions.length > 5) {\n    similarQuestions.length = 5;\n  }\n  return (\n    <Accordion defaultActiveKey=\"0\" className=\"search-question-wrap mt-3\">\n      <Accordion.Item eventKey=\"0\" className=\"overflow-hidden\">\n        <Accordion.Button className=\"px-3 py-2 bg-light text-body\">\n          {t('similar_questions')}\n        </Accordion.Button>\n\n        <Accordion.Body className=\"p-0\">\n          <ListGroup variant=\"flush\">\n            {similarQuestions.map((item) => {\n              return (\n                <ListGroup.Item\n                  action\n                  as={Link}\n                  className=\"link-dark text-wrap text-break\"\n                  key={item.id}\n                  to={pathFactory.questionLanding(item.id, item.url_title)}\n                  target=\"_blank\">\n                  <span\n                    className={`${\n                      item.accepted_answer || item.answer_count > 0\n                        ? 'me-3'\n                        : ''\n                    }`}>\n                    {item.title}\n                    {item.status === 'closed'\n                      ? ` [${t('closed', { keyPrefix: 'question' })}] `\n                      : null}\n                  </span>\n\n                  {item.accepted_answer ? (\n                    <span className=\"small text-success d-inline-block\">\n                      <Icon type=\"bi\" name=\"check-circle-fill\" />\n                      <span className=\"ms-1\">\n                        {t('x_answers', {\n                          keyPrefix: 'question',\n                          count: item.answer_count,\n                        })}\n                      </span>\n                    </span>\n                  ) : (\n                    item.answer_count > 0 && (\n                      <span className=\"small text-secondary d-inline-block\">\n                        <Icon type=\"bi\" name=\"chat-square-text-fill\" />\n                        <span className=\"ms-1\">\n                          {t('x_answers', {\n                            keyPrefix: 'question',\n                            count: item.answer_count,\n                          })}\n                        </span>\n                      </span>\n                    )\n                  )}\n                </ListGroup.Item>\n              );\n            })}\n          </ListGroup>\n        </Accordion.Body>\n      </Accordion.Item>\n    </Accordion>\n  );\n};\n\nexport default memo(SearchQuestion);\n"
  },
  {
    "path": "ui/src/pages/Questions/Ask/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState, useEffect, useRef, useCallback } from 'react';\nimport { Row, Col, Form, Button, Card } from 'react-bootstrap';\nimport { useParams, useNavigate, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport dayjs from 'dayjs';\nimport classNames from 'classnames';\nimport isEqual from 'lodash/isEqual';\nimport debounce from 'lodash/debounce';\nimport fm from 'front-matter';\n\nimport { writeSettingStore } from '@/stores';\nimport { usePageTags, usePromptWithUnload } from '@/hooks';\nimport { Editor, EditorRef, TagSelector } from '@/components';\nimport type * as Type from '@/common/interface';\nimport { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';\nimport {\n  saveQuestion,\n  questionDetail,\n  modifyQuestion,\n  useQueryRevisions,\n  queryQuestionByTitle,\n  getTagsBySlugName,\n  saveQuestionWithAnswer,\n} from '@/services';\nimport {\n  handleFormError,\n  SaveDraft,\n  storageExpires,\n  scrollToElementTop,\n} from '@/utils';\nimport { pathFactory } from '@/router/pathFactory';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\n\nimport SearchQuestion from './components/SearchQuestion';\n\ninterface FormDataItem {\n  title: Type.FormValue<string>;\n  tags: Type.FormValue<Type.Tag[]>;\n  content: Type.FormValue<string>;\n  answer_content: Type.FormValue<string>;\n  edit_summary: Type.FormValue<string>;\n}\n\nconst saveDraft = new SaveDraft({ type: 'question' });\n\nconst Ask = () => {\n  const initFormData = {\n    title: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    tags: {\n      value: [],\n      isInvalid: false,\n      errorMsg: '',\n    },\n    content: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    answer_content: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    edit_summary: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  };\n  const { t } = useTranslation('translation', { keyPrefix: 'ask' });\n  const [formData, setFormData] = useState<FormDataItem>(initFormData);\n  const [immData, setImmData] = useState<FormDataItem>(initFormData);\n  const [checked, setCheckState] = useState(false);\n  const [blockState, setBlockState] = useState(false);\n  const [focusType, setForceType] = useState('');\n  const [hasDraft, setHasDraft] = useState(false);\n  const resetForm = () => {\n    setFormData(initFormData);\n    setCheckState(false);\n    setForceType('');\n  };\n  const [similarQuestions, setSimilarQuestions] = useState([]);\n\n  const editorRef = useRef<EditorRef>({\n    getHtml: () => '',\n  });\n  const editorRef2 = useRef<EditorRef>({\n    getHtml: () => '',\n  });\n\n  const { qid } = useParams();\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const updateTags = (tags: string) => {\n    getTagsBySlugName(tags).then((resp) => {\n      // eslint-disable-next-line\n      handleTagsChange(resp);\n    });\n  };\n  const writeInfo = writeSettingStore((state) => state.write);\n\n  const isEdit = qid !== undefined;\n\n  const saveCaptcha = useCaptchaPlugin('question');\n  const editCaptcha = useCaptchaPlugin('edit');\n\n  const removeDraft = () => {\n    saveDraft.save.cancel();\n    saveDraft.remove();\n    setHasDraft(false);\n  };\n\n  useEffect(() => {\n    if (!qid) {\n      // order: 1. tags query. 2. prefill query. 3. draft\n      const queryTags = searchParams.get('tags');\n      if (queryTags) {\n        updateTags(queryTags);\n      }\n      const draft = storageExpires.get(DRAFT_QUESTION_STORAGE_KEY);\n\n      const prefill = searchParams.get('prefill');\n      if (prefill || draft) {\n        if (prefill) {\n          const file = fm<any>(decodeURIComponent(prefill));\n          formData.title.value = file.attributes?.title;\n          formData.content.value = file.body;\n          if (!queryTags && file.attributes?.tags) {\n            // Remove spaces in file.attributes.tags\n            const filterTags = file.attributes.tags\n              .split(',')\n              .map((tag) => tag.trim())\n              .join(',');\n            updateTags(filterTags);\n          }\n        } else if (draft) {\n          formData.title.value = draft.title;\n          formData.content.value = draft.content;\n          formData.tags.value = draft.tags;\n          formData.answer_content.value = draft.answer_content;\n          setCheckState(Boolean(draft.answer_content));\n          setHasDraft(true);\n        }\n        setFormData({ ...formData });\n      } else {\n        resetForm();\n      }\n    }\n\n    return () => {\n      resetForm();\n    };\n  }, [qid]);\n\n  useEffect(() => {\n    const { title, tags, content, answer_content } = formData;\n    const { title: editTitle, tags: editTags, content: editContent } = immData;\n\n    // edited\n    if (qid) {\n      if (\n        editTitle.value !== title.value ||\n        editContent.value !== content.value ||\n        !isEqual(\n          editTags.value.map((v) => v.slug_name),\n          tags.value.map((v) => v.slug_name),\n        )\n      ) {\n        setBlockState(true);\n      } else {\n        setBlockState(false);\n      }\n      return;\n    }\n    // write\n    if (\n      title.value ||\n      tags.value.length > 0 ||\n      content.value ||\n      answer_content.value\n    ) {\n      // save draft\n      saveDraft.save({\n        params: {\n          title: title.value,\n          tags: tags.value,\n          content: content.value,\n          answer_content: answer_content.value,\n        },\n        callback: () => setHasDraft(true),\n      });\n      setBlockState(true);\n    } else {\n      removeDraft();\n      setBlockState(false);\n    }\n  }, [formData]);\n\n  usePromptWithUnload({\n    when: blockState,\n  });\n\n  const { data: revisions = [] } = useQueryRevisions(qid);\n\n  useEffect(() => {\n    if (!isEdit) {\n      return;\n    }\n    questionDetail(qid).then((res) => {\n      formData.title.value = res.title;\n      formData.content.value = res.content;\n      formData.tags.value = res.tags.map((item) => {\n        return {\n          ...item,\n          parsed_text: '',\n          original_text: '',\n        };\n      });\n      setImmData({ ...formData });\n      setFormData({ ...formData });\n    });\n  }, [qid]);\n\n  const querySimilarQuestions = useCallback(\n    debounce((title) => {\n      queryQuestionByTitle(title).then((res) => {\n        setSimilarQuestions(res);\n      });\n    }, 400),\n    [],\n  );\n\n  const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      title: { value: e.currentTarget.value, errorMsg: '', isInvalid: false },\n    });\n    if (e.currentTarget.value.length >= 10) {\n      querySimilarQuestions(e.currentTarget.value);\n    }\n    if (e.currentTarget.value.length === 0) {\n      setSimilarQuestions([]);\n    }\n  };\n  const handleContentChange = (value: string) => {\n    setFormData((prev) => ({\n      ...prev,\n      content: { value, errorMsg: '', isInvalid: false },\n    }));\n  };\n  const handleTagsChange = (value) =>\n    setFormData({\n      ...formData,\n      tags: { value, errorMsg: '', isInvalid: false },\n    });\n\n  const handleAnswerChange = (value: string) =>\n    setFormData((prev) => ({\n      ...prev,\n      answer_content: { value, errorMsg: '', isInvalid: false },\n    }));\n\n  const handleSummaryChange = (evt: React.ChangeEvent<HTMLInputElement>) =>\n    setFormData({\n      ...formData,\n      edit_summary: {\n        ...formData.edit_summary,\n        value: evt.currentTarget.value,\n      },\n    });\n\n  const deleteDraft = () => {\n    const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));\n    if (res) {\n      removeDraft();\n      resetForm();\n    }\n  };\n\n  const submitModifyQuestion = (params) => {\n    setBlockState(false);\n    const ep = {\n      ...params,\n      id: qid,\n      edit_summary: formData.edit_summary.value,\n    };\n    const imgCode = editCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      ep.captcha_code = imgCode.captcha_code;\n      ep.captcha_id = imgCode.captcha_id;\n    }\n    modifyQuestion(ep)\n      .then(async (res) => {\n        await editCaptcha?.close();\n        navigate(pathFactory.questionLanding(qid, res?.url_title), {\n          state: { isReview: res?.wait_for_review },\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          editCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const submitQuestion = async (params) => {\n    setBlockState(false);\n    const imgCode = saveCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      params.captcha_code = imgCode.captcha_code;\n      params.captcha_id = imgCode.captcha_id;\n    }\n    let res;\n    if (checked) {\n      res = await saveQuestionWithAnswer({\n        ...params,\n        answer_content: formData.answer_content.value,\n      }).catch((err) => {\n        if (err.isError) {\n          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);\n          if (!(captchaErr && err.list.length === 1)) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n            const ele = document.getElementById(err.list[0].error_field);\n            scrollToElementTop(ele);\n          }\n        }\n      });\n    } else {\n      res = await saveQuestion(params).catch((err) => {\n        if (err.isError) {\n          const captchaErr = saveCaptcha?.handleCaptchaError(err.list);\n          if (!(captchaErr && err.list.length === 1)) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n            const ele = document.getElementById(err.list[0].error_field);\n            scrollToElementTop(ele);\n          }\n        }\n      });\n    }\n\n    const id = res?.id || res?.question?.id;\n    if (id) {\n      await saveCaptcha?.close();\n      if (checked) {\n        navigate(pathFactory.questionLanding(id, res?.question?.url_title));\n      } else {\n        navigate(pathFactory.questionLanding(id, res?.url_title));\n      }\n    }\n    removeDraft();\n  };\n\n  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const params: Type.QuestionParams = {\n      title: formData.title.value,\n      content: formData.content.value,\n      tags: formData.tags.value,\n    };\n\n    if (isEdit) {\n      if (!editCaptcha) {\n        submitModifyQuestion(params);\n        return;\n      }\n      editCaptcha.check(() => submitModifyQuestion(params));\n    } else {\n      if (!saveCaptcha) {\n        submitQuestion(params);\n        return;\n      }\n      saveCaptcha?.check(async () => {\n        submitQuestion(params);\n      });\n    }\n  };\n  const backPage = () => {\n    navigate(-1);\n  };\n\n  const handleSelectedRevision = (e) => {\n    const index = e.target.value;\n    const revision = revisions[index];\n    formData.content.value = revision.content?.content || '';\n    setImmData({ ...formData });\n    setFormData({ ...formData });\n  };\n  const bool = similarQuestions.length > 0 && !isEdit;\n  let pageTitle = t('ask_a_question', { keyPrefix: 'page_title' });\n  if (isEdit) {\n    pageTitle = t('edit_question', { keyPrefix: 'page_title' });\n  }\n  usePageTags({\n    title: pageTitle,\n  });\n\n  const handleContentHint = () => {\n    if (\n      !writeInfo ||\n      writeInfo.min_content === undefined ||\n      !writeInfo.min_content\n    ) {\n      return t(`form.fields.body.hint.optional_body`);\n    }\n\n    return t(`form.fields.body.hint.minimum_characters`, {\n      min_content_length: writeInfo.min_content,\n    });\n  };\n\n  return (\n    <div className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{isEdit ? t('edit_title') : t('title')}</h3>\n      <Row>\n        <Col className=\"page-main flex-auto\">\n          <Form noValidate onSubmit={handleSubmit}>\n            {isEdit && (\n              <Form.Group controlId=\"revision\" className=\"mb-3\">\n                <Form.Label>{t('form.fields.revision.label')}</Form.Label>\n                <Form.Select onChange={handleSelectedRevision}>\n                  {revisions.map(({ reason, create_at, user_info }, index) => {\n                    const date = dayjs(create_at * 1000)\n                      .tz()\n                      .format(t('long_date_with_time', { keyPrefix: 'dates' }));\n                    return (\n                      <option key={`${create_at}`} value={index}>\n                        {`${date} - ${user_info.display_name} - ${\n                          reason ||\n                          (index === revisions.length - 1\n                            ? t('default_first_reason')\n                            : t('default_reason'))\n                        }`}\n                      </option>\n                    );\n                  })}\n                </Form.Select>\n              </Form.Group>\n            )}\n            <Form.Group controlId=\"title\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.title.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.title.value}\n                isInvalid={formData.title.isInvalid}\n                onChange={handleTitleChange}\n                placeholder={t('form.fields.title.placeholder')}\n                autoFocus\n                contentEditable\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.title.errorMsg}\n              </Form.Control.Feedback>\n              {bool && <SearchQuestion similarQuestions={similarQuestions} />}\n            </Form.Group>\n            <Form.Group controlId=\"content\">\n              <Form.Label>{t('form.fields.body.label')}</Form.Label>\n              <Editor\n                value={formData.content.value}\n                onChange={handleContentChange}\n                className={classNames(\n                  'form-control p-0',\n                  focusType === 'content' && 'focus',\n                  formData.content.isInvalid && 'is-invalid',\n                )}\n                onFocus={() => {\n                  setForceType('content');\n                }}\n                onBlur={() => {\n                  setForceType('');\n                }}\n                ref={editorRef}\n              />\n              <Form.Text>{handleContentHint()}</Form.Text>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.content.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"tags\" className=\"my-3\">\n              <Form.Label>{t('form.fields.tags.label')}</Form.Label>\n              <TagSelector\n                value={formData.tags.value}\n                onChange={handleTagsChange}\n                showRequiredTag\n                maxTagLength={5}\n                isInvalid={formData.tags.isInvalid}\n                errMsg={formData.tags.errorMsg}\n              />\n            </Form.Group>\n            {!isEdit && (\n              <>\n                <Form.Switch\n                  checked={checked}\n                  type=\"switch\"\n                  label={t('answer_question')}\n                  onChange={(e) => setCheckState(e.target.checked)}\n                  id=\"radio-answer\"\n                />\n                {checked && (\n                  <Form.Group controlId=\"answer\" className=\"mt-3\">\n                    <Form.Label>{t('form.fields.answer.label')}</Form.Label>\n                    <Editor\n                      value={formData.answer_content.value}\n                      onChange={handleAnswerChange}\n                      ref={editorRef2}\n                      className={classNames(\n                        'form-control p-0',\n                        focusType === 'answer' && 'focus',\n                        formData.answer_content.isInvalid && 'is-invalid',\n                      )}\n                      onFocus={() => {\n                        setForceType('answer');\n                      }}\n                      onBlur={() => {\n                        setForceType('');\n                      }}\n                    />\n                    <Form.Control\n                      type=\"text\"\n                      isInvalid={formData.answer_content.isInvalid}\n                      hidden\n                    />\n                    <Form.Control.Feedback type=\"invalid\">\n                      {formData.answer_content.errorMsg}\n                    </Form.Control.Feedback>\n                  </Form.Group>\n                )}\n              </>\n            )}\n            {isEdit && (\n              <Form.Group controlId=\"edit_summary\" className=\"my-3\">\n                <Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>\n                <Form.Control\n                  type=\"text\"\n                  defaultValue={formData.edit_summary.value}\n                  isInvalid={formData.edit_summary.isInvalid}\n                  placeholder={t('form.fields.edit_summary.placeholder')}\n                  onChange={handleSummaryChange}\n                  contentEditable\n                />\n                <Form.Control.Feedback type=\"invalid\">\n                  {formData.edit_summary.errorMsg}\n                </Form.Control.Feedback>\n              </Form.Group>\n            )}\n            {!checked && (\n              <div className=\"mt-3\">\n                <Button type=\"submit\" className=\"me-2\">\n                  {isEdit ? t('btn_save_edits') : t('btn_post_question')}\n                </Button>\n                {isEdit && (\n                  <Button variant=\"link\" onClick={backPage}>\n                    {t('cancel', { keyPrefix: 'btns' })}\n                  </Button>\n                )}\n\n                {hasDraft && (\n                  <Button variant=\"link\" onClick={deleteDraft}>\n                    {t('discard_draft', { keyPrefix: 'btns' })}\n                  </Button>\n                )}\n              </div>\n            )}\n            {checked && (\n              <div className=\"mt-3\">\n                <Button type=\"submit\">{t('post_question&answer')}</Button>\n                {hasDraft && (\n                  <Button variant=\"link\" className=\"ms-2\" onClick={deleteDraft}>\n                    {t('discard_draft', { keyPrefix: 'btns' })}\n                  </Button>\n                )}\n              </div>\n            )}\n          </Form>\n        </Col>\n        <Col className=\"page-right-side mt-4 mt-xl-0\">\n          <Card>\n            <Card.Header>\n              {t('title', { keyPrefix: 'how_to_format' })}\n            </Card.Header>\n            <Card.Body\n              className=\"fmt small\"\n              dangerouslySetInnerHTML={{\n                __html: t('desc', { keyPrefix: 'how_to_format' }),\n              }}\n            />\n          </Card>\n        </Col>\n      </Row>\n    </div>\n  );\n};\n\nexport default Ask;\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/Alert/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Alert } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport dayjs from 'dayjs';\n\ninterface Props {\n  data;\n}\nconst Index: FC<Props> = ({ data }) => {\n  const { t } = useTranslation();\n  return (\n    <Alert className=\"mb-4\" variant={data.level}>\n      {data.level === 'info' ? (\n        <div>\n          {data.msg.startsWith('http') ? (\n            <p>\n              {data.description}{' '}\n              <a href={data.msg} className=\"alert-exist\">\n                <strong>{t('question_detail.show_exist')}</strong>\n              </a>\n            </p>\n          ) : (\n            <p>{data.msg ? data.msg : data.description}</p>\n          )}\n          <div className=\"small\">\n            {t('question_detail.closed_in')}{' '}\n            <time\n              dateTime={dayjs.unix(data.time).tz().toISOString()}\n              title={dayjs\n                .unix(data.time)\n                .tz()\n                .format(t('dates.long_date_with_time'))}>\n              {dayjs\n                .unix(data.time)\n                .tz()\n                .format(t('dates.long_date_with_year'))}\n            </time>\n            .\n          </div>\n        </div>\n      ) : (\n        data.msg\n      )}\n    </Alert>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/Answer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useEffect, useRef } from 'react';\nimport { Button, Alert, Badge } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams } from 'react-router-dom';\n\nimport {\n  Actions,\n  Operate,\n  UserCard,\n  Icon,\n  Comment,\n  htmlRender,\n  ImgViewer,\n} from '@/components';\nimport { scrollToElementTop, bgFadeOut } from '@/utils';\nimport { AnswerItem } from '@/common/interface';\nimport { acceptanceAnswer } from '@/services';\nimport { useRenderHtmlPlugin } from '@/utils/pluginKit';\n\ninterface Props {\n  data: AnswerItem;\n  /** router answer id */\n  aid?: string;\n  canAccept: boolean;\n  questionTitle: string;\n  isLogged: boolean;\n  callback: (type: string) => void;\n}\nconst Index: FC<Props> = ({\n  aid,\n  data,\n  isLogged,\n  questionTitle = '',\n  callback,\n  canAccept = false,\n}) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'question_detail',\n  });\n  const [searchParams] = useSearchParams();\n  const answerRef = useRef<HTMLDivElement>(null);\n\n  useRenderHtmlPlugin(answerRef.current?.querySelector('.fmt') as HTMLElement);\n\n  const acceptAnswer = () => {\n    acceptanceAnswer({\n      question_id: data.question_id,\n      answer_id: data.accepted === 2 ? '0' : data.id,\n    }).then(() => {\n      callback?.('');\n    });\n  };\n\n  useEffect(() => {\n    if (!answerRef?.current) {\n      return;\n    }\n\n    htmlRender(answerRef.current.querySelector('.fmt'), {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [answerRef.current]);\n\n  useEffect(() => {\n    if (aid === data.id) {\n      setTimeout(() => {\n        const element = answerRef.current;\n        scrollToElementTop(element);\n        if (!searchParams.get('commentId')) {\n          bgFadeOut(answerRef.current);\n        }\n      }, 100);\n    }\n  }, [data.id]);\n\n  if (!data?.id) {\n    return null;\n  }\n\n  return (\n    <div id={data.id} ref={answerRef} className=\"answer-item py-4\">\n      {data.status === 10 && (\n        <Alert variant=\"danger\" className=\"mb-4\">\n          {t('post_deleted', { keyPrefix: 'messages' })}\n        </Alert>\n      )}\n      {data.status === 11 && (\n        <Alert variant=\"secondary\" className=\"mb-4\">\n          {t('post_pending', { keyPrefix: 'messages' })}\n        </Alert>\n      )}\n      <div className=\"d-flex justify-content-between mb-3\">\n        <div style={{ minWidth: '196px' }}>\n          <UserCard\n            data={data?.user_info}\n            time={Number(data.create_time)}\n            updateTime={Number(data.update_time)}\n            updateTimePrefix={t('edit')}\n            isLogged={isLogged}\n            timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}\n          />\n        </div>\n\n        {data?.accepted === 2 && (\n          <div className=\"lh-1\">\n            <Badge bg=\"success\" pill>\n              <Icon name=\"check-circle-fill me-1\" />\n              Best answer\n            </Badge>\n          </div>\n        )}\n      </div>\n      <ImgViewer>\n        <article\n          className=\"fmt text-break text-wrap\"\n          dangerouslySetInnerHTML={{ __html: data?.html }}\n        />\n      </ImgViewer>\n      <div className=\"d-flex align-items-center my-4\">\n        <Actions\n          source=\"answer\"\n          data={{\n            id: data?.id,\n            isHate: data?.vote_status === 'vote_down',\n            isLike: data?.vote_status === 'vote_up',\n            votesCount: data?.vote_count,\n            hideCollect: true,\n            collected: data?.collected,\n            collectCount: 0,\n            username: data?.user_info?.username,\n          }}\n        />\n\n        {canAccept && (\n          <Button\n            variant={data.accepted === 2 ? 'success' : 'outline-success'}\n            className=\"ms-3\"\n            onClick={acceptAnswer}>\n            <Icon name=\"check-circle-fill\" className=\"me-2\" />\n            <span>\n              {data.accepted === 2\n                ? t('answers.btn_accepted')\n                : t('answers.btn_accept')}\n            </span>\n          </Button>\n        )}\n      </div>\n\n      <Comment\n        objectId={data.id}\n        mode=\"answer\"\n        commentId={searchParams.get('commentId')}>\n        <Operate\n          qid={data.question_id}\n          aid={data.id}\n          memberActions={data?.member_actions}\n          type=\"answer\"\n          isAccepted={data.accepted === 2}\n          title={questionTitle}\n          callback={callback}\n        />\n      </Comment>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { QueryGroup } from '@/components';\n\ninterface Props {\n  count: number;\n  order: string;\n}\n\nconst sortBtns = [\n  {\n    name: 'score',\n    sort: 'default',\n  },\n  {\n    name: 'newest',\n    sort: 'updated',\n  },\n  {\n    name: 'oldest',\n    sort: 'created',\n  },\n];\n\nconst Index: FC<Props> = ({ count = 0, order = 'default' }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'question_detail.answers',\n  });\n\n  return (\n    <div\n      className=\"d-flex align-items-center justify-content-between mt-5 mb-3\"\n      id=\"answerHeader\">\n      <h5 className=\"mb-0\">\n        {count} {t('title')}\n      </h5>\n      <QueryGroup\n        data={sortBtns}\n        currentSort={\n          order === 'updated'\n            ? 'newest'\n            : order === 'created'\n              ? 'oldest'\n              : 'score'\n        }\n        i18nKeyPrefix=\"question_detail.answers\"\n      />\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/ContentLoader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\n\nconst Index: FC = () => {\n  return (\n    <div className=\"placeholder-glow\">\n      <div className=\"placeholder w-100 h1 mb-3\" style={{ height: '34px' }} />\n\n      <div className=\"placeholder w-75 mb-3\" style={{ height: '21px' }} />\n\n      <div\n        className=\"placeholder w-50 d-block align-top mb-4\"\n        style={{ height: '24px' }}\n      />\n\n      <div style={{ marginBottom: '2rem' }}>\n        <p>\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-75 d-block align-top\"\n            style={{ height: '24px' }}\n          />\n        </p>\n\n        <p>\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-100 d-block align-top mb-1\"\n            style={{ height: '24px' }}\n          />\n          <span\n            className=\"placeholder w-50 d-block align-top\"\n            style={{ height: '24px' }}\n          />\n        </p>\n      </div>\n\n      <div className=\"d-flex\">\n        <div\n          className=\"placeholder align-top me-3 rounded\"\n          style={{ height: '38px', width: '120px' }}\n        />\n        <div\n          className=\"placeholder align-top rounded\"\n          style={{ height: '38px', width: '68px' }}\n        />\n      </div>\n\n      <div className=\"d-block d-md-flex flex-wrap mt-4 mb-3\">\n        <div\n          className=\"placeholder mb-3 mb-md-0 me-4\"\n          style={{ height: '21px', width: '40%' }}\n        />\n        <div\n          style={{ minWidth: '196px', height: '24px' }}\n          className=\"placeholder mb-3 me-4 mb-md-0 d-block d-md-none\"\n        />\n\n        <div\n          style={{ minWidth: '196px', height: '24px' }}\n          className=\"placeholder d-block d-md-none\"\n        />\n\n        <div\n          style={{ minWidth: '196px', height: '40px' }}\n          className=\"placeholder mb-3 me-4 mb-md-0 d-none d-md-block\"\n        />\n\n        <div\n          style={{ minWidth: '196px', height: '40px' }}\n          className=\"placeholder d-none d-md-block\"\n        />\n      </div>\n\n      {[0, 1, 2].map((item, i) => (\n        <div\n          className={`border-bottom py-2 ${i === 0 ? 'border-top' : ''}`}\n          key={item}>\n          <div className=\"placeholder w-100 mb-1\" style={{ height: '17px' }} />\n          <div className=\"placeholder w-50\" style={{ height: '17px' }} />\n        </div>\n      ))}\n\n      <div className=\"d-flex mt-2 mb-4\">\n        <div\n          className=\"placeholder align-top me-4\"\n          style={{ height: '21px', width: '140px' }}\n        />\n        <div\n          className=\"placeholder align-top\"\n          style={{ height: '21px', width: '140px' }}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.people-dropdown {\n  position: absolute;\n  top: 34px;\n  left: 0;\n  width: 100%;\n\n  .dropdown-item.active {\n    color: var(--bs-body-color);\n    background-color: var(--an-invite-answer-item-active);\n  }\n  .check-cover {\n    width: 18px;\n    height: 18px;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    margin: auto 0;\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Dropdown, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { loggedUserInfoStore } from '@/stores';\nimport { userSearchByName } from '@/services';\nimport { Avatar } from '@/components';\nimport * as Type from '@/common/interface';\nimport './PeopleDropdown.scss';\n\ninterface Props {\n  selectedPeople: Type.UserInfoBase[] | undefined;\n  onSelect: (people: Type.UserInfoBase) => void;\n  saveInviteUsers: () => void;\n  visible?: boolean;\n}\n\ninterface UserInfoCheck extends Type.UserInfoBase {\n  checked?: boolean;\n}\n\nconst Index: FC<Props> = ({\n  selectedPeople = [],\n  visible = false,\n  onSelect,\n  saveInviteUsers,\n}) => {\n  const { user: currentUser } = loggedUserInfoStore();\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'invite_to_answer',\n  });\n  const [peopleList, setPeopleList] = useState<UserInfoCheck[]>([]);\n  const [currentIndex, setCurrentIndex] = useState(-1);\n  const [searchValue, setSearchValue] = useState('');\n  const filterAndSetPeople = (source) => {\n    const filteredPeople: Type.UserInfoBase[] = [];\n    source.forEach((p) => {\n      if (\n        currentUser &&\n        currentUser.role_id === 1 &&\n        currentUser.username === p.username\n      ) {\n        return;\n      }\n      if (selectedPeople?.find((_) => _.username === p.username)) {\n        return;\n      }\n      filteredPeople.push(p);\n    });\n    setPeopleList([...selectedPeople, ...filteredPeople]);\n  };\n\n  const searchPeople = (s) => {\n    if (!s) {\n      setPeopleList([...selectedPeople]);\n      return;\n    }\n    userSearchByName(s).then((resp) => {\n      filterAndSetPeople(resp);\n    });\n  };\n  const handleSearch = (evt) => {\n    const s = evt.target.value;\n    setSearchValue(s);\n    searchPeople(s);\n  };\n\n  const resetSearch = () => {\n    setCurrentIndex(-1);\n    setSearchValue('');\n    setPeopleList([]);\n  };\n\n  const handleSelect = (idx) => {\n    if (idx < 0 || idx >= peopleList.length) {\n      return;\n    }\n    const people = peopleList[idx];\n    if (people) {\n      onSelect(people);\n    }\n  };\n\n  const handleKeyDown = (evt) => {\n    evt.stopPropagation();\n\n    if (!peopleList?.length) {\n      return;\n    }\n    const { keyCode } = evt;\n    if (keyCode === 38 && currentIndex > 0) {\n      setCurrentIndex(currentIndex - 1);\n    }\n    if (keyCode === 40 && currentIndex < peopleList.length - 1) {\n      setCurrentIndex(currentIndex + 1);\n    }\n\n    if (keyCode === 13 && currentIndex > -1) {\n      evt.preventDefault();\n      handleSelect(currentIndex);\n    }\n  };\n\n  useEffect(() => {\n    if (visible && selectedPeople.length > 0) {\n      setPeopleList(selectedPeople);\n    }\n    if (!visible) {\n      resetSearch();\n    }\n  }, [visible]);\n\n  return visible ? (\n    <Dropdown\n      autoClose=\"outside\"\n      show\n      className=\"people-dropdown\"\n      onSelect={handleSelect}\n      onKeyDown={handleKeyDown}\n      onToggle={(state) => {\n        if (!state) {\n          saveInviteUsers();\n        }\n      }}>\n      <Dropdown.Menu\n        renderOnMount\n        show\n        className=\"w-100 py-0 position-relative\">\n        <div className=\"p-3\">\n          <Form.Control\n            type=\"search\"\n            autoFocus\n            placeholder={t('search')}\n            value={searchValue}\n            onChange={handleSearch}\n          />\n        </div>\n        <div className={`${peopleList.length > 0 ? 'py-2 border-top' : ''}`}>\n          {peopleList.map((p, idx) => {\n            return (\n              <Dropdown.Item\n                key={p.username}\n                eventKey={idx}\n                onFocus={() => {\n                  setCurrentIndex(idx);\n                }}\n                active={idx === currentIndex}>\n                <Form.Check\n                  type=\"checkbox\"\n                  id={p.username}\n                  className=\"position-relative\">\n                  <Form.Check.Input\n                    type=\"checkbox\"\n                    tabIndex={-1}\n                    checked={Boolean(\n                      selectedPeople?.find((v) => v.id === p.id),\n                    )}\n                    onChange={() => {}}\n                  />\n                  <div className=\"check-cover\" />\n                  <Form.Check.Label className=\"d-flex align-items-center text-nowrap\">\n                    <Avatar\n                      avatar={p.avatar}\n                      size=\"24\"\n                      alt={p.display_name}\n                      className=\"rounded-1\"\n                    />\n                    <div className=\"d-flex flex-wrap text-truncate\">\n                      <span className=\"ms-2 text-truncate\">\n                        {p.display_name}\n                      </span>\n                      <small className=\"text-secondary text-truncate ms-2\">\n                        @{p.username}\n                      </small>\n                    </div>\n                  </Form.Check.Label>\n                </Form.Check>\n              </Dropdown.Item>\n            );\n          })}\n        </div>\n      </Dropdown.Menu>\n    </Dropdown>\n  ) : null;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/InviteToAnswer/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.invite-answer-card {\n  .card-header {\n    border: var(--bs-border-width) solid var(--bs-border-color-translucent);\n  }\n\n  .card-body {\n    border: var(--bs-border-width) solid var(--bs-border-color-translucent);\n    border-top: 0;\n    border-bottom-left-radius: var(--bs-border-radius);\n    border-bottom-right-radius: var(--bs-border-radius);\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState, useEffect } from 'react';\nimport { Card, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { Avatar } from '@/components';\nimport { getInviteUser, putInviteUser } from '@/services';\nimport type * as Type from '@/common/interface';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\n\nimport PeopleDropdown from './PeopleDropdown';\n\nimport './index.scss';\n\ninterface Props {\n  questionId: string;\n  readOnly?: boolean;\n}\nconst Index: FC<Props> = ({ questionId, readOnly = false }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'invite_to_answer',\n  });\n\n  const [editing, setEditing] = useState(false);\n  const [users, setUsers] = useState<Type.UserInfoBase[]>([]);\n  const iaCaptcha = useCaptchaPlugin('invitation_answer');\n\n  const initInviteUsers = () => {\n    if (!questionId) {\n      return;\n    }\n    getInviteUser(questionId)\n      .then((resp) => {\n        setUsers(resp);\n      })\n      .catch(() => {\n        if (!users) {\n          setUsers([]);\n        }\n      });\n  };\n\n  const updateInviteUsers = (user: Type.UserInfoBase) => {\n    const userID = users?.find((_) => _.id === user.id);\n    let userList: any = [...(users || [])];\n    if (userID) {\n      userList = userList?.filter((_) => {\n        return _.id !== user.id;\n      });\n    } else {\n      userList.push(user);\n    }\n    setUsers(userList);\n  };\n\n  const submitInviteUser = () => {\n    const names = users.map((_) => {\n      return _.username;\n    });\n    const imgCode: Type.ImgCodeReq = {};\n    iaCaptcha?.resolveCaptchaReq(imgCode);\n    putInviteUser(questionId, names, imgCode)\n      .then(async () => {\n        await iaCaptcha?.close();\n        setEditing(false);\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          iaCaptcha?.handleCaptchaError(ex.list);\n        }\n      });\n  };\n\n  const saveInviteUsers = () => {\n    if (!users) {\n      return;\n    }\n\n    if (!iaCaptcha) {\n      submitInviteUser();\n      return;\n    }\n\n    iaCaptcha.check(() => submitInviteUser());\n  };\n\n  useEffect(() => {\n    initInviteUsers();\n  }, [questionId]);\n\n  const showEmpty = readOnly && users?.length === 0;\n\n  if (showEmpty) {\n    return null;\n  }\n\n  return (\n    <Card className=\"invite-answer-card position-relative border-0 mb-4\">\n      <Card.Header className=\"text-nowrap d-flex justify-content-between text-capitalize\">\n        {t('title')}\n        {!readOnly && (\n          <Button\n            onClick={() => setEditing(true)}\n            variant=\"link\"\n            className=\"p-0\">\n            {t('edit', { keyPrefix: 'btns' })}\n          </Button>\n        )}\n      </Card.Header>\n      <Card.Body className={classNames('position-relative')}>\n        <div className={classNames('d-flex align-items-center flex-wrap m-n1')}>\n          {users?.map((user) => {\n            return (\n              <Link\n                key={user.username}\n                to={`/users/${user.username}`}\n                className=\"mx-2 my-1 d-inline-flex flex-nowrap\">\n                <Avatar\n                  avatar={user.avatar}\n                  size=\"24\"\n                  alt={user.display_name}\n                  className=\"rounded-1\"\n                />\n                <small className=\"ms-2\">{user.display_name}</small>\n              </Link>\n            );\n          })}\n          {users?.length === 0 ? (\n            <div className=\"text-muted\">{t('desc')}</div>\n          ) : null}\n        </div>\n      </Card.Body>\n      {editing && (\n        <PeopleDropdown\n          visible={editing}\n          selectedPeople={users}\n          onSelect={updateInviteUsers}\n          saveInviteUsers={saveInviteUsers}\n        />\n      )}\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/LinkedQuestions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Card, ListGroup } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { isEmpty } from 'lodash';\n\nimport { Icon } from '@/components';\nimport { useQuestionLink } from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  id: string;\n}\nconst Index: FC<Props> = ({ id }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'linked_question',\n  });\n  const { t: t2 } = useTranslation('translation', {\n    keyPrefix: 'related_question',\n  });\n\n  const { data } = useQuestionLink({\n    question_id: id,\n    page: 1,\n    page_size: 5,\n  });\n\n  if (!data || isEmpty(data?.list)) {\n    return null;\n  }\n\n  return (\n    <Card className=\"mb-4\">\n      <Card.Header className=\"text-nowrap d-flex justify-content-between text-capitalize\">\n        {t('title')}\n        <Link to={`/linked/${id}`} className=\"btn btn-link p-0\">\n          {t('more', { keyPrefix: 'btns' })}\n        </Link>\n      </Card.Header>\n      <ListGroup variant=\"flush\">\n        {data.list?.map((item) => (\n          <ListGroup.Item\n            action\n            key={item.id}\n            as={Link}\n            to={pathFactory.questionLanding(item.id, item.url_title)}>\n            <div className=\"link-dark text-truncate-3\">{item.title}</div>\n            {item.answer_count > 0 && (\n              <div\n                className={`mt-1 small me-2 ${\n                  item.accepted_answer_id > 0\n                    ? 'link-success'\n                    : 'link-secondary'\n                }`}>\n                <Icon\n                  name={\n                    item.accepted_answer_id > 0\n                      ? 'check-circle-fill'\n                      : 'chat-square-text-fill'\n                  }\n                  className=\"me-1\"\n                />\n                <span>\n                  {item.answer_count} {t2('answers')}\n                </span>\n              </div>\n            )}\n          </ListGroup.Item>\n        ))}\n      </ListGroup>\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/Question/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState, useEffect, useRef } from 'react';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\nimport { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';\n\nimport {\n  Tag,\n  Actions,\n  Operate,\n  BaseUserCard,\n  Comment,\n  FormatTime,\n  htmlRender,\n  ImgViewer,\n} from '@/components';\nimport { useRenderHtmlPlugin } from '@/utils/pluginKit';\nimport { formatCount, guard } from '@/utils';\nimport { following } from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  data: any;\n  hasAnswer: boolean;\n  initPage: (type: string) => void;\n  isLogged: boolean;\n}\n\nconst Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'question_detail',\n  });\n  const [searchParams] = useSearchParams();\n  const [followed, setFollowed] = useState(data?.is_followed);\n  const ref = useRef<HTMLDivElement>(null);\n\n  useRenderHtmlPlugin(ref.current);\n\n  const handleFollow = (e) => {\n    e.preventDefault();\n    if (!guard.tryNormalLogged(true)) {\n      return;\n    }\n    following({\n      object_id: data?.id,\n      is_cancel: followed,\n    }).then((res) => {\n      setFollowed(res.is_followed);\n    });\n  };\n\n  useEffect(() => {\n    if (data) {\n      setFollowed(data?.is_followed);\n    }\n  }, [data]);\n\n  useEffect(() => {\n    if (!ref.current) {\n      return;\n    }\n\n    htmlRender(ref.current, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [ref.current]);\n\n  if (!data?.id) {\n    return null;\n  }\n\n  return (\n    <div>\n      <h1 className=\"h3 mb-2 text-wrap text-break pb-1\">\n        <Link\n          className=\"link-dark\"\n          reloadDocument\n          to={pathFactory.questionLanding(data.id, data.url_title)}>\n          {data.title}\n          {data.status === 2\n            ? ` [${t('closed', { keyPrefix: 'question' })}]`\n            : ''}\n        </Link>\n      </h1>\n\n      <div className=\"d-flex flex-wrap align-items-center small mb-4 text-secondary border-bottom pb-3\">\n        <BaseUserCard data={data.user_info} className=\"me-3\" />\n\n        {isLogged ? (\n          <>\n            <Link to={`/posts/${data.id}/timeline`}>\n              <FormatTime\n                time={data.create_time}\n                preFix={t('created')}\n                className=\"me-3 link-secondary\"\n              />\n            </Link>\n\n            <Link to={`/posts/${data.id}/timeline`}>\n              <FormatTime\n                time={data.edit_time}\n                preFix={t('Edited')}\n                className=\"me-3 link-secondary\"\n              />\n            </Link>\n          </>\n        ) : (\n          <>\n            <FormatTime\n              time={data.create_time}\n              preFix={t('created')}\n              className=\"me-3 link-secondary\"\n            />\n\n            <FormatTime\n              time={data.edit_time}\n              preFix={t('Edited')}\n              className=\"me-3 link-secondary\"\n            />\n          </>\n        )}\n\n        {data?.view_count > 0 && (\n          <div className=\"me-3\">\n            {t('Views')} {formatCount(data.view_count)}\n          </div>\n        )}\n        <OverlayTrigger\n          placement=\"bottom\"\n          overlay={<Tooltip id=\"followTooltip\">{t('follow_tip')}</Tooltip>}>\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            className=\"p-0 btn-no-border\"\n            onClick={(e) => handleFollow(e)}>\n            {t(followed ? 'Following' : 'Follow')}\n          </Button>\n        </OverlayTrigger>\n      </div>\n\n      <ImgViewer>\n        <article\n          ref={ref}\n          className=\"fmt text-break text-wrap last-p mb-4\"\n          dangerouslySetInnerHTML={{ __html: data?.html }}\n        />\n      </ImgViewer>\n\n      <div className=\"m-n1\">\n        {data?.tags?.map((item: any) => {\n          return <Tag className=\"m-1\" key={item.slug_name} data={item} />;\n        })}\n      </div>\n\n      <Actions\n        className=\"mt-4\"\n        source=\"question\"\n        data={{\n          id: data?.id,\n          isHate: data?.vote_status === 'vote_down',\n          isLike: data?.vote_status === 'vote_up',\n          votesCount: data?.vote_count,\n          collected: data?.collected,\n          collectCount: data?.collection_count,\n          username: data.user_info?.username,\n        }}\n      />\n\n      <div className=\"mt-4\">\n        <Comment\n          objectId={data?.id}\n          mode=\"question\"\n          commentId={searchParams.get('commentId')}>\n          <Operate\n            qid={data?.id}\n            type=\"question\"\n            memberActions={data?.member_actions}\n            title={data.title}\n            hasAnswer={hasAnswer}\n            isAccepted={Boolean(data?.accepted_answer_id)}\n            callback={initPage}\n          />\n        </Comment>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/Reactions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect, useState } from 'react';\nimport { Button, OverlayTrigger, Popover, Tooltip } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon } from '@/components';\nimport { queryReactions, updateReaction } from '@/services';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { isDarkTheme } from '@/utils/common';\nimport { ReactionItem } from '@/common/interface';\n\ninterface Props {\n  objectId: string;\n  showAddCommentBtn?: boolean;\n  handleClickComment: () => void;\n}\n\nconst emojiMap = [\n  {\n    name: 'heart',\n    icon: 'heart-fill',\n    className: 'text-danger',\n  },\n  {\n    name: 'smile',\n    icon: 'emoji-laughing-fill',\n    className: 'text-warning',\n  },\n  {\n    name: 'frown',\n    icon: 'emoji-frown-fill',\n    className: 'text-warning',\n  },\n];\n\nconst Index: FC<Props> = ({\n  objectId,\n  showAddCommentBtn,\n  handleClickComment,\n}) => {\n  const [reactions, setReactions] = useState<ReactionItem[]>();\n  const [reactIsActive, setReactIsActive] = useState<boolean>(false);\n  const { t } = useTranslation('translation');\n  const darkMode = isDarkTheme();\n\n  useEffect(() => {\n    queryReactions(objectId).then((res) => {\n      setReactions(res?.reaction_summary);\n    });\n  }, []);\n\n  const handleSubmit = (params: { object_id: string; emoji: string }) => {\n    if (!tryNormalLogged(true)) {\n      setReactIsActive(false);\n      return;\n    }\n    updateReaction({\n      ...params,\n      reaction: reactions?.find((v) => v.emoji === params.emoji)?.is_active\n        ? 'deactivate'\n        : 'activate',\n    }).then((res) => {\n      setReactions(res.reaction_summary);\n      setReactIsActive(false);\n    });\n  };\n\n  const renderPopover = (props) => (\n    <Popover id=\"reaction-button-tooltip\" {...props}>\n      <Popover.Body className=\"d-block d-md-flex flex-wrap p-1\">\n        {emojiMap.map((d, index) => (\n          <Button\n            aria-label={\n              reactions?.find((v) => v.emoji === d.name)?.is_active\n                ? t('reaction.undo_emoji', { emoji: d.name })\n                : t(`reaction.${d.name}`)\n            }\n            key={d.icon}\n            variant={darkMode ? 'dark' : 'light'}\n            active={reactions?.find((v) => v.emoji === d.name)?.is_active}\n            className={`${index !== 0 ? 'ms-1' : ''}`}\n            size=\"sm\"\n            onClick={() =>\n              handleSubmit({ object_id: objectId, emoji: d.name })\n            }>\n            <Icon name={d.icon} className={d.className} />\n          </Button>\n        ))}\n      </Popover.Body>\n    </Popover>\n  );\n\n  return (\n    <div className=\"d-flex flex-wrap\">\n      {showAddCommentBtn && (\n        <Button\n          className=\"rounded-pill me-2 link-secondary btn-reaction\"\n          variant={darkMode ? 'dark' : 'light'}\n          size=\"sm\"\n          onClick={handleClickComment}>\n          <Icon name=\"chat-text-fill\" />\n          <span className=\"ms-1\">{t('comment.btn_add_comment')}</span>\n        </Button>\n      )}\n\n      <OverlayTrigger\n        trigger=\"click\"\n        placement=\"top\"\n        overlay={renderPopover}\n        show={reactIsActive}\n        onToggle={(show) => setReactIsActive(show)}>\n        <Button\n          size=\"sm\"\n          aria-label={t('reaction.btn_label')}\n          aria-haspopup=\"true\"\n          active={reactIsActive}\n          className=\"smile-btn rounded-pill link-secondary btn-reaction\"\n          variant={darkMode ? 'dark' : 'light'}>\n          <Icon name=\"emoji-smile-fill\" />\n          <span className=\"ms-1\">+</span>\n        </Button>\n      </OverlayTrigger>\n\n      {reactions?.map((data) => {\n        if (!data.emoji || data?.count <= 0) {\n          return null;\n        }\n        return (\n          <OverlayTrigger\n            key={data.emoji}\n            placement=\"top\"\n            overlay={\n              <Tooltip>\n                <div className=\"text-start\">\n                  <b>{t(`reaction.${data.emoji}`)}</b> <br /> {data.tooltip}\n                </div>\n              </Tooltip>\n            }>\n            <Button\n              className=\"rounded-pill ms-2 link-secondary d-flex align-items-center btn-reaction\"\n              aria-label={\n                data?.is_active\n                  ? t('reaction.unreact_emoji', { emoji: data.emoji })\n                  : t('reaction.react_emoji', { emoji: data.emoji })\n              }\n              aria-pressed=\"true\"\n              variant={darkMode ? 'dark' : 'light'}\n              active={data.is_active}\n              size=\"sm\"\n              onClick={() =>\n                handleSubmit({ object_id: objectId, emoji: data.emoji })\n              }>\n              <Icon\n                name={String(emojiMap.find((v) => v.name === data.emoji)?.icon)}\n                className={\n                  emojiMap.find((v) => v.name === data.emoji)?.className\n                }\n              />\n              <span className=\"ms-1 lh-1\">{data.count}</span>\n            </Button>\n          </OverlayTrigger>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Card, ListGroup } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon } from '@/components';\nimport { useSimilarQuestion } from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  id: string;\n}\nconst Index: FC<Props> = ({ id }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'related_question',\n  });\n\n  const { data, isLoading } = useSimilarQuestion({\n    question_id: id,\n    page_size: 5,\n  });\n\n  if (isLoading) {\n    return null;\n  }\n\n  if (!isLoading && data?.count === 0) {\n    return null;\n  }\n\n  return (\n    <Card>\n      <Card.Header>{t('title')}</Card.Header>\n      <ListGroup variant=\"flush\">\n        {data?.list?.map((item) => {\n          return (\n            <ListGroup.Item\n              action\n              key={item.id}\n              as={Link}\n              to={pathFactory.questionLanding(item.id, item.url_title)}>\n              <div className=\"link-dark text-truncate-3\">{item.title}</div>\n              {item.answer_count > 0 && (\n                <div\n                  className={`mt-1 small me-2 ${\n                    item.accepted_answer_id > 0\n                      ? 'link-success'\n                      : 'link-secondary'\n                  }`}>\n                  <Icon\n                    name={\n                      item.accepted_answer_id > 0\n                        ? 'check-circle-fill'\n                        : 'chat-square-text-fill'\n                    }\n                    className=\"me-1\"\n                  />\n\n                  <span>\n                    {item.answer_count} {t('answers')}\n                  </span>\n                </div>\n              )}\n            </ListGroup.Item>\n          );\n        })}\n      </ListGroup>\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, useState, FC, useEffect } from 'react';\nimport { Form, Button, Alert } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { marked } from 'marked';\nimport classNames from 'classnames';\n\nimport { usePromptWithUnload } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { Editor, Modal, TextArea } from '@/components';\nimport { FormDataType, PostAnswerReq } from '@/common/interface';\nimport { postAnswer } from '@/services';\nimport { guard, handleFormError, SaveDraft, storageExpires } from '@/utils';\nimport { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants';\nimport { writeSettingStore } from '@/stores';\n\ninterface Props {\n  visible?: boolean;\n  data: {\n    /** question  id */\n    qid: string;\n    answered?: boolean;\n    loggedUserRank: number;\n    first_answer_id?: string;\n  };\n  callback?: (obj) => void;\n}\n\nconst saveDraft = new SaveDraft({ type: 'answer' });\n\nconst Index: FC<Props> = ({ visible = false, data, callback }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'question_detail.write_answer',\n  });\n  const [formData, setFormData] = useState<FormDataType>({\n    content: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n  const [showEditor, setShowEditor] = useState<boolean>(visible);\n  const [focusType, setFocusType] = useState('');\n  const [editorFocusState, setEditorFocusState] = useState(false);\n  const [hasDraft, setHasDraft] = useState(false);\n  const [showTips, setShowTips] = useState(data.loggedUserRank < 100);\n  const aCaptcha = useCaptchaPlugin('answer');\n  const writeInfo = writeSettingStore((state) => state.write);\n  const [editorCanSave, setEditorCanSave] = useState(false);\n\n  usePromptWithUnload({\n    when: Boolean(formData.content.value),\n  });\n\n  const removeDraft = () => {\n    // immediately remove debounced save\n    saveDraft.save.cancel();\n    saveDraft.remove();\n    setHasDraft(false);\n  };\n\n  useEffect(() => {\n    const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);\n    if (draft?.questionId === data.qid && draft?.content) {\n      setShowEditor(true);\n      setFormData({\n        content: {\n          value: draft.content,\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n      setHasDraft(true);\n    }\n    setTimeout(() => {\n      setEditorCanSave(true);\n    }, 100);\n  }, []);\n\n  useEffect(() => {\n    const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY);\n    const { content } = formData;\n\n    if (content.value) {\n      // save Draft\n      saveDraft.save({\n        questionId: data?.qid,\n        content: content.value,\n      });\n\n      setHasDraft(true);\n    } else if (draft?.questionId === data.qid && !content.value) {\n      removeDraft();\n    }\n  }, [formData.content.value]);\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { content } = formData;\n\n    if (!content.value || Array.from(content.value.trim()).length < 6) {\n      bol = false;\n      formData.content = {\n        value: content.value,\n        isInvalid: true,\n        errorMsg: t('characters'),\n      };\n    } else {\n      formData.content = {\n        value: content.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n    return bol;\n  };\n\n  const resetForm = () => {\n    setFormData({\n      content: {\n        value: '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const deleteDraft = () => {\n    const res = window.confirm(t('discard_confirm', { keyPrefix: 'draft' }));\n    if (res) {\n      removeDraft();\n      resetForm();\n    }\n  };\n\n  const submitAnswer = () => {\n    const params: PostAnswerReq = {\n      question_id: data?.qid,\n      content: formData.content.value,\n      html: marked.parse(formData.content.value),\n    };\n    const imgCode = aCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      params.captcha_code = imgCode.captcha_code;\n      params.captcha_id = imgCode.captcha_id;\n    }\n    postAnswer(params)\n      .then(async (res) => {\n        await aCaptcha?.close();\n        setShowEditor(false);\n        setFormData({\n          content: {\n            value: '',\n            isInvalid: false,\n            errorMsg: '',\n          },\n        });\n        removeDraft();\n        callback?.(res.info);\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          aCaptcha?.handleCaptchaError(ex.list);\n          const stateData = handleFormError(ex, formData);\n          setFormData({ ...stateData });\n        }\n      });\n  };\n\n  const handleSubmit = () => {\n    if (!guard.tryNormalLogged(true)) {\n      return;\n    }\n    if (!checkValidated()) {\n      return;\n    }\n    if (!aCaptcha) {\n      submitAnswer();\n      return;\n    }\n    aCaptcha.check(() => submitAnswer());\n  };\n\n  const clickBtn = () => {\n    if (!guard.tryNormalLogged(true)) {\n      return;\n    }\n\n    if (data?.answered && !showEditor) {\n      Modal.confirm({\n        title: t('confirm_title'),\n        content: t('confirm_info'),\n        confirmText: t('continue'),\n        onConfirm: () => {\n          setShowEditor(true);\n        },\n      });\n      return;\n    }\n\n    if (!showEditor) {\n      setShowEditor(true);\n      return;\n    }\n\n    handleSubmit();\n  };\n  const handleFocusForTextArea = (evt) => {\n    if (!guard.tryNormalLogged(true)) {\n      evt.currentTarget.blur();\n      return;\n    }\n    setFocusType('answer');\n    setShowEditor(true);\n    setEditorFocusState(true);\n  };\n\n  return (\n    <Form noValidate className=\"mt-4\">\n      {(!data.answered || showEditor) && (\n        <Form.Group className=\"mb-3\">\n          <Form.Label>\n            <h5>{t('title')}</h5>\n          </Form.Label>\n          <Form.Control\n            isInvalid={formData.content.isInvalid}\n            className=\"d-none\"\n          />\n          {!showEditor && !data.answered && (\n            <div className=\"d-flex\">\n              <TextArea\n                className=\"w-100\"\n                rows={8}\n                autoFocus={false}\n                onFocus={handleFocusForTextArea}\n              />\n            </div>\n          )}\n          {showEditor && (\n            <>\n              <Editor\n                className={classNames(\n                  'form-control p-0',\n                  focusType === 'answer' && 'focus',\n                  formData.content.isInvalid && 'is-invalid',\n                )}\n                value={formData.content.value}\n                autoFocus={editorFocusState}\n                onChange={(val) => {\n                  if (editorCanSave) {\n                    setFormData({\n                      content: {\n                        value: val,\n                        isInvalid: false,\n                        errorMsg: '',\n                      },\n                    });\n                  }\n                }}\n                onFocus={() => {\n                  setFocusType('answer');\n                }}\n                onBlur={() => {\n                  setFocusType('');\n                }}\n              />\n\n              <Alert\n                variant=\"warning\"\n                show={data.loggedUserRank < 100 && showTips}\n                onClose={() => setShowTips(false)}\n                dismissible\n                className=\"mt-3\">\n                <p>{t('tips.header_1')}</p>\n                <ul>\n                  <li>\n                    <Trans\n                      i18nKey=\"question_detail.write_answer.tips.li1_1\"\n                      components={{ strong: <strong /> }}\n                    />\n                  </li>\n                  <li>{t('tips.li1_2')}</li>\n                </ul>\n                <p>\n                  <Trans\n                    i18nKey=\"question_detail.write_answer.tips.header_2\"\n                    components={{ strong: <strong /> }}\n                  />\n                </p>\n                <ul className=\"mb-0\">\n                  <li>{t('tips.li2_1')}</li>\n                </ul>\n              </Alert>\n            </>\n          )}\n\n          <Form.Control.Feedback type=\"invalid\">\n            {formData.content.errorMsg}\n          </Form.Control.Feedback>\n        </Form.Group>\n      )}\n\n      {data.answered && !showEditor ? (\n        // the 0th answer is the oldest one\n        <Link\n          to={`/posts/${data.qid}/${data.first_answer_id}/edit`}\n          className=\"btn btn-primary\">\n          {t('edit_answer')}\n        </Link>\n      ) : (\n        <Button onClick={clickBtn}>{t('btn_name')}</Button>\n      )}\n\n      {data.answered && !showEditor && !writeInfo.restrict_answer && (\n        <Button onClick={clickBtn} className=\"ms-2 \" variant=\"outline-primary\">\n          {t('add_another_answer')}\n        </Button>\n      )}\n\n      {hasDraft && (\n        <Button variant=\"link\" className=\"ms-2\" onClick={deleteDraft}>\n          {t('discard_draft', { keyPrefix: 'btns' })}\n        </Button>\n      )}\n    </Form>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/components/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Question from './Question';\nimport Answer from './Answer';\nimport AnswerHead from './AnswerHead';\nimport RelatedQuestions from './RelatedQuestions';\nimport WriteAnswer from './WriteAnswer';\nimport Alert from './Alert';\nimport ContentLoader from './ContentLoader';\nimport InviteToAnswer from './InviteToAnswer';\nimport LinkedQuestions from './LinkedQuestions';\n\nexport {\n  Question,\n  Answer,\n  AnswerHead,\n  RelatedQuestions,\n  WriteAnswer,\n  Alert,\n  ContentLoader,\n  InviteToAnswer,\n  LinkedQuestions,\n};\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n#reaction-button-tooltip {\n  .popover-body {\n    .btn {\n      background-color: var(--bs-body-bg);\n      border-color: transparent;\n\n      &:hover {\n        [data-bs-theme='dark'] & {\n          background-color: var(--bs-gray-800);\n        }\n\n        background-color: var(--bs-gray-100);\n      }\n\n      &:active,\n      &.active {\n        background-color: var(--bs-gray-200);\n\n        [data-bs-theme='dark'] & {\n          background-color: var(--bs-gray-700);\n        }\n      }\n    }\n  }\n}\n\n.btn-reaction {\n  [data-bs-theme='dark'] & {\n    color: var(--bs-gray-400) !important;\n    background-color: var(--bs-gray-800);\n    &:active,\n    &.active {\n      background-color: #626e79 !important;\n    }\n    &:hover {\n      background-color: #57616b !important;\n    }\n  }\n}\n\n.answer-item {\n  border-top: 1px solid var(--an-answer-item-border-top);\n}\n\n.smile-btn.active::before {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 80;\n  display: block;\n  cursor: default;\n  content: ' ';\n}\n\n@media screen and (max-width: 768px) {\n  .questionDetailPage {\n    h1.h3 {\n      font-size: calc(1.275rem + 0.3vw) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Questions/Detail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport {\n  useParams,\n  useSearchParams,\n  useNavigate,\n  useLocation,\n} from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Pagination, CustomSidebar } from '@/components';\nimport { loggedUserInfoStore, toastStore } from '@/stores';\nimport { scrollToElementTop, scrollToDocTop } from '@/utils';\nimport { usePageTags, usePageUsers, useSkeletonControl } from '@/hooks';\nimport type {\n  ListResult,\n  QuestionDetailRes,\n  AnswerItem,\n} from '@/common/interface';\nimport { questionDetail, getAnswers } from '@/services';\n\nimport {\n  Question,\n  Answer,\n  AnswerHead,\n  RelatedQuestions,\n  WriteAnswer,\n  Alert,\n  ContentLoader,\n  InviteToAnswer,\n  LinkedQuestions,\n} from './components';\n\nimport './index.scss';\n\nconst Index = () => {\n  const navigate = useNavigate();\n  const { t } = useTranslation('translation');\n  const { qid = '', slugPermalink = '' } = useParams();\n  /**\n   * Note: Compatible with Permalink\n   */\n  let { aid = '' } = useParams();\n  if (!aid && slugPermalink) {\n    aid = slugPermalink;\n  }\n\n  const [urlSearch] = useSearchParams();\n  const page = Number(urlSearch.get('page') || 0);\n  const order = urlSearch.get('order') || '';\n  const [question, setQuestion] = useState<QuestionDetailRes | null>(null);\n  const [isLoading, setIsLoading] = useState<boolean>(true);\n  const { isSkeletonShow } = useSkeletonControl(isLoading);\n  const [answers, setAnswers] = useState<ListResult<AnswerItem>>({\n    count: -1,\n    list: [],\n  });\n  const { setUsers } = usePageUsers();\n  const userInfo = loggedUserInfoStore((state) => state.user);\n  const isAuthor = userInfo?.username === question?.user_info?.username;\n  const isAdmin = userInfo?.role_id === 2;\n  const isModerator = userInfo?.role_id === 3;\n  const isLogged = Boolean(userInfo?.access_token);\n  const loggedUserRank = userInfo?.rank;\n  const location = useLocation();\n\n  useEffect(() => {\n    if (location.state?.isReview) {\n      toastStore.getState().show({\n        msg: t('review', { keyPrefix: 'toast' }),\n        variant: 'warning',\n      });\n\n      // remove state isReview\n      const newLocation = { ...location };\n      delete newLocation.state;\n      window.history.replaceState(null, '', newLocation.pathname);\n    }\n  }, [location.state]);\n\n  const requestAnswers = async () => {\n    const res = await getAnswers({\n      order: order === 'updated' || order === 'created' ? order : 'default',\n      question_id: qid,\n      page: 1,\n      page_size: 999,\n    });\n\n    if (res) {\n      res.list = res.list?.filter((v) => {\n        // delete answers only show to author and admin and has search params aid\n        if (v.status === 10) {\n          if (\n            (v?.user_info?.username === userInfo?.username || isAdmin) &&\n            aid === v.id\n          ) {\n            return v;\n          }\n          return null;\n        }\n\n        return v;\n      });\n\n      setAnswers({ ...res, count: res.list.length });\n      if (page > 0 || order) {\n        // scroll into view;\n        const element = document.getElementById('answerHeader');\n        scrollToElementTop(element);\n      }\n\n      res.list.forEach((item) => {\n        setUsers([\n          {\n            displayName: item.user_info?.display_name,\n            userName: item.user_info?.username,\n          },\n          {\n            displayName: item?.update_user_info?.display_name,\n            userName: item?.update_user_info?.username,\n          },\n        ]);\n      });\n    }\n  };\n\n  const getDetail = async () => {\n    setIsLoading(true);\n    try {\n      const res = await questionDetail(qid);\n      if (res) {\n        setUsers([\n          {\n            id: res.user_info?.id,\n            displayName: res.user_info?.display_name,\n            userName: res.user_info?.username,\n            avatar_url: res.user_info?.avatar,\n          },\n          {\n            id: res?.update_user_info?.id,\n            displayName: res?.update_user_info?.display_name,\n            userName: res?.update_user_info?.username,\n            avatar_url: res?.update_user_info?.avatar,\n          },\n          {\n            id: res?.last_answered_user_info?.id,\n            displayName: res?.last_answered_user_info?.display_name,\n            userName: res?.last_answered_user_info?.username,\n            avatar_url: res?.last_answered_user_info?.avatar,\n          },\n        ]);\n        setQuestion(res);\n      }\n      setIsLoading(false);\n    } catch (e) {\n      setIsLoading(false);\n    }\n  };\n\n  const initPage = (type: string) => {\n    if (type === 'delete_question') {\n      setTimeout(() => {\n        navigate('/', { replace: true });\n      }, 1000);\n      return;\n    }\n    if (type === 'default') {\n      scrollToDocTop();\n      getDetail();\n      return;\n    }\n    if (type === 'delete_answer') {\n      getDetail();\n    }\n    requestAnswers();\n  };\n\n  const writeAnswerCallback = (obj: AnswerItem) => {\n    setAnswers({\n      count: answers.count + 1,\n      list: [...answers.list, obj],\n    });\n\n    if (question) {\n      setQuestion({\n        ...question,\n        answered: true,\n        first_answer_id: question.first_answer_id\n          ? question.first_answer_id\n          : obj.id,\n      });\n    }\n  };\n\n  useEffect(() => {\n    if (!qid) {\n      return;\n    }\n    getDetail();\n    requestAnswers();\n  }, [qid]);\n\n  useEffect(() => {\n    if (page || order) {\n      requestAnswers();\n    }\n  }, [page, order]);\n  usePageTags({\n    title: question?.title,\n    description: question?.description,\n    keywords: question?.tags.map((_) => _.slug_name).join(','),\n  });\n\n  const showInviteToAnswer = question?.id;\n  const showLinkedQuestions = question?.id && question.id !== '';\n  let canInvitePeople = false;\n  if (showInviteToAnswer && Array.isArray(question.extends_actions)) {\n    const inviteAct = question.extends_actions.find((op) => {\n      return op.action === 'invite_other_to_answer';\n    });\n    if (inviteAct) {\n      canInvitePeople = true;\n    }\n  }\n\n  return (\n    <Row className=\"questionDetailPage pt-4 mb-5\">\n      <Col className=\"page-main flex-auto\">\n        {question?.operation?.level && <Alert data={question.operation} />}\n        {isSkeletonShow ? (\n          <ContentLoader />\n        ) : (\n          <Question\n            data={question}\n            initPage={initPage}\n            hasAnswer={answers.count > 0}\n            isLogged={isLogged}\n          />\n        )}\n        {!isLoading && answers.count > 0 && (\n          <>\n            <AnswerHead count={answers.count} order={order} />\n            {answers?.list?.map((item) => {\n              return (\n                <Answer\n                  aid={aid}\n                  key={item?.id}\n                  data={item}\n                  questionTitle={question?.title || ''}\n                  canAccept={isAuthor || isAdmin || isModerator}\n                  callback={initPage}\n                  isLogged={isLogged}\n                />\n              );\n            })}\n          </>\n        )}\n\n        {!isLoading && Math.ceil(answers.count / 15) > 1 && (\n          <div className=\"d-flex justify-content-center answer-item pt-4\">\n            <Pagination\n              currentPage={Number(page || 1)}\n              pageSize={15}\n              totalSize={answers?.count || 0}\n            />\n          </div>\n        )}\n\n        {!isLoading &&\n          Number(question?.status) !== 2 &&\n          !question?.operation?.type && (\n            <WriteAnswer\n              data={{\n                qid,\n                answered: question?.answered,\n                loggedUserRank,\n                first_answer_id: question?.first_answer_id,\n              }}\n              callback={writeAnswerCallback}\n            />\n          )}\n      </Col>\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <CustomSidebar />\n        {showInviteToAnswer ? (\n          <InviteToAnswer\n            questionId={question.id}\n            readOnly={!canInvitePeople}\n          />\n        ) : null}\n        {showLinkedQuestions ? <LinkedQuestions id={question.id} /> : null}\n        <RelatedQuestions id={question?.id || ''} />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Questions/EditAnswer/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.edit-answer-wrap {\n  .question-content-wrap {\n    position: relative;\n    margin-bottom: 20px;\n    overflow: hidden;\n\n    .content {\n      height: calc(100% - 12px);\n      overflow-y: scroll;\n      z-index: 1;\n      background: var(--an-white);\n    }\n    img {\n      max-width: 100%;\n    }\n    .scroll-bar {\n      height: 12px;\n    }\n\n    .line {\n      position: absolute;\n      transition: 0.3s;\n      pointer-events: none;\n    }\n\n    .resize-bottom {\n      overflow-y: scroll;\n      width: 16px;\n      min-height: 50px;\n      resize: vertical;\n      transform: scale(110, 1);\n      height: 100%;\n    }\n    .resize-bottom::-webkit-resizer {\n      border: none;\n    }\n    .resize-bottom + .line {\n      left: 0;\n      width: 100%;\n      height: 16px;\n      top: auto;\n      bottom: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Questions/EditAnswer/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { Row, Col, Form, Button, Card } from 'react-bootstrap';\nimport { useParams, useNavigate, Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport dayjs from 'dayjs';\nimport classNames from 'classnames';\n\nimport { handleFormError } from '@/utils';\nimport { usePageTags, usePromptWithUnload } from '@/hooks';\nimport { useCaptchaPlugin, useRenderHtmlPlugin } from '@/utils/pluginKit';\nimport { pathFactory } from '@/router/pathFactory';\nimport { Editor, EditorRef, Icon, htmlRender } from '@/components';\nimport type * as Type from '@/common/interface';\nimport {\n  useQueryAnswerInfo,\n  modifyAnswer,\n  useQueryRevisions,\n} from '@/services';\n\nimport './index.scss';\n\ninterface FormDataItem {\n  content: Type.FormValue<string>;\n  description: Type.FormValue<string>;\n}\n\nconst Index = () => {\n  const { aid = '', qid = '' } = useParams();\n  const [focusType, setForceType] = useState('');\n\n  const { t } = useTranslation('translation', { keyPrefix: 'edit_answer' });\n  const navigate = useNavigate();\n\n  const initFormData = {\n    content: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    description: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  };\n\n  const { data } = useQueryAnswerInfo(aid);\n  const [formData, setFormData] = useState<FormDataItem>(initFormData);\n  const [immData, setImmData] = useState(initFormData);\n  const [contentChanged, setContentChanged] = useState(false);\n  const editCaptcha = useCaptchaPlugin('edit');\n\n  useEffect(() => {\n    if (data?.info?.content) {\n      setFormData({\n        ...formData,\n        content: {\n          value: data.info.content,\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n    }\n  }, [data?.info?.content]);\n\n  const { data: revisions = [] } = useQueryRevisions(aid);\n\n  const editorRef = useRef<EditorRef>({\n    getHtml: () => '',\n  });\n\n  const questionContentRef = useRef<HTMLDivElement>(null);\n  useRenderHtmlPlugin(questionContentRef.current);\n\n  useEffect(() => {\n    if (!questionContentRef?.current) {\n      return;\n    }\n    htmlRender(questionContentRef.current, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [questionContentRef]);\n\n  usePromptWithUnload({\n    when: contentChanged,\n  });\n\n  useEffect(() => {\n    const { content, description } = formData;\n    if (immData.content.value !== content.value || description.value) {\n      setContentChanged(true);\n    } else {\n      setContentChanged(false);\n    }\n  }, [formData.content.value, formData.description.value]);\n\n  const handleAnswerChange = (value: string) =>\n    setFormData((prev) => ({\n      ...prev,\n      content: { ...prev.content, value },\n    }));\n  const handleSummaryChange = (evt) => {\n    const v = evt.currentTarget.value;\n    setFormData({\n      ...formData,\n      description: { ...formData.description, value: v },\n    });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { content } = formData;\n\n    if (!content.value || Array.from(content.value.trim()).length < 6) {\n      bol = false;\n      formData.content = {\n        value: content.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.answer.feedback.characters'),\n      };\n    } else {\n      formData.content = {\n        value: content.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n    return bol;\n  };\n\n  const submitEditAnswer = () => {\n    const params: Type.AnswerParams = {\n      content: formData.content.value,\n      html: editorRef.current.getHtml(),\n      question_id: qid,\n      id: aid,\n      edit_summary: formData.description.value,\n    };\n    editCaptcha?.resolveCaptchaReq(params);\n\n    modifyAnswer(params)\n      .then(async (res) => {\n        await editCaptcha?.close();\n        navigate(\n          pathFactory.answerLanding({\n            questionId: qid,\n            slugTitle: data?.question?.url_title,\n            answerId: aid,\n          }),\n          {\n            state: { isReview: res?.wait_for_review },\n          },\n        );\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          editCaptcha?.handleCaptchaError(ex.list);\n          const stateData = handleFormError(ex, formData);\n          setFormData({ ...stateData });\n        }\n      });\n  };\n\n  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n    setContentChanged(false);\n\n    event.preventDefault();\n    event.stopPropagation();\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (!editCaptcha) {\n      submitEditAnswer();\n      return;\n    }\n    editCaptcha.check(() => submitEditAnswer());\n  };\n  const handleSelectedRevision = (e) => {\n    const index = e.target.value;\n    const revision = revisions[index];\n    if (revision?.content) {\n      formData.content.value = revision.content.content;\n      setImmData({ ...formData });\n      setFormData({ ...formData });\n    }\n  };\n\n  const backPage = () => {\n    navigate(-1);\n  };\n  usePageTags({\n    title: t('edit_answer', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <div className=\"pt-4 mb-5 edit-answer-wrap\">\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <Row>\n        <Col className=\"page-main flex-auto\">\n          <Link\n            to={pathFactory.questionLanding(qid, data?.question.url_title)}\n            target=\"_blank\"\n            rel=\"noreferrer\">\n            <h5 className=\"mb-3\">{data?.question.title}</h5>\n          </Link>\n\n          <div className=\"question-content-wrap\">\n            <div\n              ref={questionContentRef}\n              className=\"content position-absolute top-0 w-100\"\n              dangerouslySetInnerHTML={{ __html: data?.question.html }}\n            />\n            <div\n              className=\"resize-bottom\"\n              style={{ maxHeight: questionContentRef?.current?.scrollHeight }}\n            />\n            <div className=\"line bg-light  d-flex justify-content-center align-items-center\">\n              <Icon type=\"bi\" name=\"grip-horizontal\" className=\"mt-1\" />\n            </div>\n          </div>\n\n          <Form noValidate onSubmit={handleSubmit}>\n            <Form.Group controlId=\"revision\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.revision.label')}</Form.Label>\n              <Form.Select onChange={handleSelectedRevision} defaultValue={0}>\n                {revisions.map(({ create_at, reason, user_info }, index) => {\n                  const date = dayjs(create_at * 1000)\n                    .tz()\n                    .format(t('long_date_with_time', { keyPrefix: 'dates' }));\n                  return (\n                    <option key={`${create_at}`} value={index}>\n                      {`${date} - ${user_info.display_name} - ${\n                        reason ||\n                        (index === revisions.length - 1\n                          ? t('default_first_reason')\n                          : t('default_reason'))\n                      }`}\n                    </option>\n                  );\n                })}\n              </Form.Select>\n            </Form.Group>\n\n            <Form.Group controlId=\"answer\" className=\"mt-3\">\n              <Form.Label>{t('form.fields.answer.label')}</Form.Label>\n              <Editor\n                value={formData.content.value}\n                onChange={handleAnswerChange}\n                className={classNames(\n                  'form-control p-0',\n                  focusType === 'answer' && 'focus',\n                )}\n                onFocus={() => {\n                  setForceType('answer');\n                }}\n                onBlur={() => {\n                  setForceType('');\n                }}\n                ref={editorRef}\n              />\n              <Form.Control\n                value={formData.content.value}\n                type=\"text\"\n                isInvalid={formData.content.isInvalid}\n                readOnly\n                hidden\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.content.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"edit_summary\" className=\"my-3\">\n              <Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                onChange={handleSummaryChange}\n                defaultValue={formData.description.value}\n                isInvalid={formData.description.isInvalid}\n                placeholder={t('form.fields.edit_summary.placeholder')}\n                contentEditable\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.description.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <div className=\"mt-3\">\n              <Button type=\"submit\" className=\"me-2\">\n                {t('btn_save_edits')}\n              </Button>\n              <Button variant=\"link\" onClick={backPage}>\n                {t('btn_cancel')}\n              </Button>\n            </div>\n          </Form>\n        </Col>\n        <Col className=\"page-right-side mt-4 mt-xl-0\">\n          <Card>\n            <Card.Header>\n              {t('title', { keyPrefix: 'how_to_format' })}\n            </Card.Header>\n            <Card.Body\n              className=\"fmt small\"\n              dangerouslySetInnerHTML={{\n                __html: t('desc', { keyPrefix: 'how_to_format' }),\n              }}\n            />\n          </Card>\n        </Col>\n      </Row>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Questions/Linked/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useParams, useSearchParams, Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { useQuestionLink, questionDetail } from '@/services';\nimport * as Type from '@/common/interface';\nimport {\n  QuestionList,\n  CustomSidebar,\n  HotQuestions,\n  FollowingTags,\n} from '@/components';\nimport { userCenter, floppyNavigation } from '@/utils';\nimport { QUESTION_ORDER_KEYS } from '@/components/QuestionList';\nimport {\n  loggedUserInfoStore,\n  siteInfoStore,\n  loginSettingStore,\n} from '@/stores';\n\nconst LinkedQuestions: FC = () => {\n  const { qid } = useParams<{ qid: string }>();\n  const { t } = useTranslation('translation', { keyPrefix: 'linked_question' });\n  const { t: t2 } = useTranslation('translation');\n  const { user: loggedUser } = loggedUserInfoStore((_) => _);\n  const [urlSearchParams] = useSearchParams();\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const curOrder = (urlSearchParams.get('order') ||\n    QUESTION_ORDER_KEYS[0]) as Type.QuestionOrderBy;\n  const pageSize = 10;\n  const { siteInfo } = siteInfoStore();\n  const { data: listData, isLoading: listLoading } = useQuestionLink({\n    question_id: qid || '',\n    page: curPage,\n    page_size: pageSize,\n    order: curOrder,\n  });\n  const { login: loginSetting } = loginSettingStore();\n  const [questionTitle, setQuestionTitle] = useState('');\n\n  useEffect(() => {\n    questionDetail(qid || '')\n      .then((res) => {\n        setQuestionTitle(res.title);\n      })\n      .catch((err) => {\n        console.error('get question detail failed:', err);\n        setQuestionTitle(`#${qid}`);\n      });\n  }, []);\n  usePageTags({\n    title: t('title'),\n  });\n\n  console.log(\n    'listData',\n    QUESTION_ORDER_KEYS.filter((v) => v !== 'unanswered').slice(0, 5),\n  );\n\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col className=\"page-main flex-auto\">\n        <h3 className=\"mb-3\">{t('title')}</h3>\n        <div className=\"mb-5\">\n          {t('description')}&nbsp;\n          <a href={`/questions/${qid}`}>{questionTitle}</a>\n        </div>\n        <QuestionList\n          source=\"linked\"\n          data={listData}\n          order={curOrder}\n          orderList={QUESTION_ORDER_KEYS.filter(\n            (v) => v !== 'unanswered',\n          ).slice(0, 5)}\n          isLoading={listLoading}\n        />\n      </Col>\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <CustomSidebar />\n        {!loggedUser.username && (\n          <div className=\"card mb-4\">\n            <div className=\"card-body\">\n              <h5 className=\"card-title\">\n                {t2('website_welcome', {\n                  site_name: siteInfo.name,\n                })}\n              </h5>\n              <p className=\"card-text\">{siteInfo.description}</p>\n              <Link\n                to={userCenter.getLoginUrl()}\n                className=\"btn btn-primary\"\n                onClick={floppyNavigation.handleRouteLinkClick}>\n                {t('login', { keyPrefix: 'btns' })}\n              </Link>\n              {loginSetting.allow_new_registrations ? (\n                <Link\n                  to={userCenter.getSignUpUrl()}\n                  className=\"btn btn-link ms-2\"\n                  onClick={floppyNavigation.handleRouteLinkClick}>\n                  {t('signup', { keyPrefix: 'btns' })}\n                </Link>\n              ) : null}\n            </div>\n          </div>\n        )}\n        {loggedUser.access_token && <FollowingTags />}\n        <HotQuestions />\n      </Col>\n    </Row>\n  );\n};\n\nexport default LinkedQuestions;\n"
  },
  {
    "path": "ui/src/pages/Questions/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useMatch, Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport {\n  FollowingTags,\n  QuestionList,\n  HotQuestions,\n  CustomSidebar,\n} from '@/components';\nimport {\n  siteInfoStore,\n  loggedUserInfoStore,\n  loginSettingStore,\n} from '@/stores';\nimport { useQuestionList, useQuestionRecommendList } from '@/services';\nimport * as Type from '@/common/interface';\nimport { userCenter, floppyNavigation } from '@/utils';\nimport { QUESTION_ORDER_KEYS } from '@/components/QuestionList';\n\nconst Questions: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  const { t: t2 } = useTranslation('translation');\n  const { user: loggedUser } = loggedUserInfoStore((_) => _);\n  const [urlSearchParams] = useSearchParams();\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const curOrder = (urlSearchParams.get('order') ||\n    QUESTION_ORDER_KEYS[0]) as Type.QuestionOrderBy;\n  const reqParams: Type.QueryQuestionsReq = {\n    page_size: 20,\n    page: curPage,\n    order: curOrder as Type.QuestionOrderBy,\n  };\n  const { data: listData, isLoading: listLoading } =\n    curOrder === 'recommend'\n      ? useQuestionRecommendList(reqParams)\n      : useQuestionList(reqParams);\n  const isIndexPage = useMatch('/');\n  let pageTitle = t('questions', { keyPrefix: 'page_title' });\n  let slogan = '';\n  const { siteInfo } = siteInfoStore();\n  if (isIndexPage) {\n    pageTitle = `${siteInfo.name}`;\n    slogan = `${siteInfo.short_description}`;\n  }\n  const { login: loginSetting } = loginSettingStore();\n\n  usePageTags({ title: pageTitle, subtitle: slogan });\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col className=\"page-main flex-auto overflow-x-hidden\">\n        <QuestionList\n          source=\"questions\"\n          data={listData}\n          order={curOrder}\n          orderList={\n            loggedUser.username\n              ? QUESTION_ORDER_KEYS\n              : QUESTION_ORDER_KEYS.filter((key) => key !== 'recommend')\n          }\n          isLoading={listLoading}\n        />\n      </Col>\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <CustomSidebar />\n        {!loggedUser.username && (\n          <div className=\"card mb-4\">\n            <div className=\"card-body\">\n              <h5 className=\"card-title\">\n                {t2('website_welcome', {\n                  site_name: siteInfo.name,\n                })}\n              </h5>\n              <p className=\"card-text\">{siteInfo.description}</p>\n              <Link\n                to={userCenter.getLoginUrl()}\n                className=\"btn btn-primary\"\n                onClick={floppyNavigation.handleRouteLinkClick}>\n                {t('login', { keyPrefix: 'btns' })}\n              </Link>\n              {loginSetting.allow_new_registrations ? (\n                <Link\n                  to={userCenter.getSignUpUrl()}\n                  className=\"btn btn-link ms-2\"\n                  onClick={floppyNavigation.handleRouteLinkClick}>\n                  {t('signup', { keyPrefix: 'btns' })}\n                </Link>\n              ) : null}\n            </div>\n          </div>\n        )}\n        {loggedUser.access_token && <FollowingTags />}\n        <HotQuestions />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Questions;\n"
  },
  {
    "path": "ui/src/pages/Review/components/ApproveDropdown/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState } from 'react';\nimport { Dropdown, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal } from '@/components';\nimport { putFlagReviewAction } from '@/services';\nimport { useReportModal, useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport type * as Type from '@/common/interface';\nimport EditPostModal from '../EditPostModal';\n\ninterface IProps {\n  itemData: Type.FlagReviewItem | null;\n  curFilter: string;\n  objectType: Type.FlagReviewItem['object_type'] | '';\n  approveCallback: () => void;\n}\n\nconst Index: FC<IProps> = ({\n  itemData,\n  objectType,\n  curFilter,\n  approveCallback,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n\n  const [isLoading, setIsLoading] = useState(false);\n  const [showEditPostModal, setShowEditPostModal] = useState(false);\n  const closeModal = useReportModal(approveCallback);\n  const toast = useToast();\n  const dCaptcha = useCaptchaPlugin('delete');\n\n  const handleEditPostModalState = () => {\n    setShowEditPostModal(!showEditPostModal);\n  };\n\n  const submitReviewAction = () => {\n    const req: Type.PutFlagReviewParams = {\n      operation_type: 'delete_post',\n      flag_id: String(itemData?.flag_id),\n      captcha_code: undefined,\n      captcha_id: undefined,\n    };\n    dCaptcha?.resolveCaptchaReq(req);\n\n    delete req.captcha_code;\n    delete req.captcha_id;\n\n    putFlagReviewAction(req)\n      .then(async () => {\n        await dCaptcha?.close();\n        let msg = '';\n        if (objectType === 'question') {\n          msg = t('post_deleted', { keyPrefix: 'messages' });\n        }\n        if (objectType === 'answer') {\n          msg = t('tip_answer_deleted');\n        }\n        if (objectType === 'answer' || objectType === 'question') {\n          toast.onShow({\n            msg,\n            variant: 'success',\n          });\n        }\n        approveCallback();\n      })\n      .catch((ex) => {\n        if (ex.isError) {\n          dCaptcha?.handleCaptchaError(ex.list);\n        }\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  const handleDelete = () => {\n    let content = '';\n\n    setIsLoading(true);\n\n    if (objectType === 'question') {\n      content =\n        Number(itemData?.answer_count) > 0\n          ? t('question', { keyPrefix: 'delete' })\n          : t('other', { keyPrefix: 'delete' });\n    }\n    if (objectType === 'answer') {\n      content = itemData?.answer_accepted\n        ? t('answer_accepted', { keyPrefix: 'delete' })\n        : t('other', { keyPrefix: 'delete' });\n    }\n    if (objectType === 'comment') {\n      content = t('other', { keyPrefix: 'delete' });\n    }\n    Modal.confirm({\n      title: t('title', { keyPrefix: 'delete' }),\n      content,\n      cancelBtnVariant: 'link',\n      confirmBtnVariant: 'danger',\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      onConfirm: () => {\n        if (!dCaptcha) {\n          submitReviewAction();\n          return;\n        }\n        dCaptcha.check(() => submitReviewAction());\n      },\n      onCancel: () => {\n        setIsLoading(false);\n      },\n    });\n  };\n\n  const handleAction = (type) => {\n    if (type === 'delete') {\n      handleDelete();\n    }\n\n    if (type === 'close') {\n      closeModal.onShow({\n        type: 'question',\n        id: itemData?.flag_id || '',\n        action: 'close',\n        source: 'review',\n        content: itemData?.reason_content,\n        reportType: itemData?.reason.reason_type || -1,\n      });\n    }\n\n    if (type === 'unlist') {\n      const keyPrefix = 'question_detail.unlist';\n      Modal.confirm({\n        title: t('title', { keyPrefix }),\n        content: t('content', { keyPrefix }),\n        cancelBtnVariant: 'link',\n        confirmText: t('confirm_btn', { keyPrefix }),\n        onConfirm: () => {\n          putFlagReviewAction({\n            operation_type: 'unlist_post',\n            flag_id: itemData?.flag_id || '',\n          }).then(() => {\n            toast.onShow({\n              msg: t(`post_${type}`, { keyPrefix: 'messages' }),\n              variant: 'success',\n            });\n            approveCallback();\n          });\n        },\n      });\n    }\n  };\n\n  const handleActionEdit = () => {\n    handleEditPostModalState();\n  };\n\n  return (\n    <div>\n      <Dropdown>\n        <Dropdown.Toggle\n          as={Button}\n          disabled={isLoading}\n          variant=\"outline-primary\"\n          id=\"dropdown-basic\">\n          {t('approve', { keyPrefix: 'btns' })}\n        </Dropdown.Toggle>\n\n        <Dropdown.Menu>\n          <Dropdown.Item onClick={() => handleActionEdit()}>\n            {t('edit_post')}\n          </Dropdown.Item>\n          {curFilter === 'normal' && objectType === 'question' && (\n            <Dropdown.Item onClick={() => handleAction('close')}>\n              {t('close', { keyPrefix: 'btns' })}\n            </Dropdown.Item>\n          )}\n          {curFilter !== 'deleted' && (\n            <Dropdown.Item onClick={() => handleAction('delete')}>\n              {t('delete', { keyPrefix: 'btns' })}\n            </Dropdown.Item>\n          )}\n          {objectType === 'question' && (\n            <>\n              <Dropdown.Divider />\n              {itemData?.object_show_status !== 2 && (\n                <Dropdown.Item onClick={() => handleAction('unlist')}>\n                  {t('unlist_post')}\n                </Dropdown.Item>\n              )}\n            </>\n          )}\n        </Dropdown.Menu>\n      </Dropdown>\n      <EditPostModal\n        visible={showEditPostModal}\n        handleClose={handleEditPostModalState}\n        objectType={objectType}\n        originalData={{\n          flag_id: itemData?.flag_id || '',\n          id: itemData?.object_id || '',\n          title: itemData?.title || '',\n          content: itemData?.original_text || '',\n          tags: itemData?.tags || [],\n          question_id: itemData?.question_id || '',\n          answer_id: itemData?.answer_id || '',\n        }}\n        callback={approveCallback}\n      />\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/EditPostModal/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.edit-post-modal {\n  max-width: 746px;\n  width: 58.3333% !important;\n}\n"
  },
  {
    "path": "ui/src/pages/Review/components/EditPostModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect } from 'react';\nimport { Modal, Button, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { putFlagReviewAction } from '@/services';\nimport { usePageUsers } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { Editor, TagSelector, Mentions, TextArea } from '@/components';\nimport {\n  // matchedUsers,\n  parseUserInfo,\n  handleFormError,\n  parseEditMentionUser,\n  scrollToElementTop,\n} from '@/utils';\nimport type * as Type from '@/common/interface';\n\nimport './index.scss';\n\ninterface Props {\n  originalData: {\n    id: string;\n    flag_id: string;\n    question_id?: string;\n    answer_id?: string;\n    title: string;\n    content: string;\n    tags: Type.Tag[];\n  };\n  objectType: Type.FlagReviewItem['object_type'] | '';\n  visible: boolean;\n  handleClose: () => void;\n  callback?: () => void;\n}\n\ninterface FormDataItem {\n  title: Type.FormValue<string>;\n  tags: Type.FormValue<Type.Tag[]>;\n  content: Type.FormValue<string>;\n}\n\nconst initFormData = {\n  title: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n  tags: {\n    value: [],\n    isInvalid: false,\n    errorMsg: '',\n  },\n  content: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n};\n\nconst Index: FC<Props> = ({\n  originalData,\n  visible = false,\n  objectType,\n  handleClose,\n  callback,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ask' });\n  const [formData, setFormData] = useState<FormDataItem>(initFormData);\n  const [focusEditor, setFocusEditor] = useState(false);\n  const [loaded, setLoaded] = useState(false);\n  const pageUsers = usePageUsers();\n\n  const editCaptcha = useCaptchaPlugin('edit');\n\n  const onClose = (bol) => {\n    if (bol) {\n      callback?.();\n    }\n    setFormData(initFormData);\n    handleClose();\n    setLoaded(false);\n  };\n\n  const handleInput = (data: Partial<FormDataItem>) => {\n    if (!loaded) {\n      return;\n    }\n    setFormData({\n      ...formData,\n      ...data,\n    });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { title, tags, content } = formData;\n    if (objectType === 'question') {\n      if (!title.value) {\n        bol = false;\n        formData.title = {\n          value: title.value,\n          isInvalid: true,\n          errorMsg: t('form.fields.title.msg.empty', {\n            keyPrefix: 'ask',\n          }),\n        };\n      }\n\n      if (!tags.value.length) {\n        bol = false;\n        formData.tags = {\n          value: tags.value,\n          isInvalid: true,\n          errorMsg: t('form.fields.tags.msg.empty', {\n            keyPrefix: 'ask',\n          }),\n        };\n      }\n    }\n\n    if (!content.value || Array.from(content.value.trim()).length < 6) {\n      bol = false;\n      formData.content = {\n        value: content.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.answer.feedback.characters', {\n          keyPrefix: 'edit_answer',\n        }),\n      };\n    } else {\n      formData.content = {\n        value: content.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const submitFlagReviewAction = () => {\n    const params: Type.PutFlagReviewParams = {\n      title: formData.title.value,\n      content: formData.content.value,\n      tags: formData.tags.value,\n      operation_type: 'edit_post',\n      flag_id: originalData.flag_id,\n    };\n    if (objectType === 'answer') {\n      delete params.title;\n      delete params.tags;\n    }\n    if (objectType === 'comment') {\n      const { value } = formData.content;\n      // const users = matchedUsers(value);\n      // const userNames = unionBy(users.map((user) => user.userName));\n      const commentMarkDown = parseUserInfo(value);\n\n      // params.mention_username_list = userNames;\n      params.content = commentMarkDown;\n\n      delete params.title;\n      delete params.tags;\n    }\n    if (objectType === 'question') {\n      const imgCode = editCaptcha?.getCaptcha();\n      if (imgCode?.verify) {\n        params.captcha_code = imgCode.captcha_code;\n        params.captcha_id = imgCode.captcha_id;\n      }\n    }\n    putFlagReviewAction(params)\n      .then(async () => {\n        await editCaptcha?.close();\n        onClose(true);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          editCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (!editCaptcha) {\n      submitFlagReviewAction();\n      return;\n    }\n\n    editCaptcha.check(() => submitFlagReviewAction());\n  };\n\n  const handleSelected = (val) => {\n    if (!loaded) {\n      return;\n    }\n    setFormData({\n      ...formData,\n      content: {\n        value: val,\n        errorMsg: '',\n        isInvalid: false,\n      },\n    });\n  };\n\n  useEffect(() => {\n    if (!visible) {\n      return;\n    }\n\n    formData.title.value = originalData.title;\n    formData.content.value = originalData.content;\n    formData.tags.value = originalData.tags.map((item) => {\n      return {\n        ...item,\n        parsed_text: '',\n        original_text: '',\n      };\n    });\n    setFormData({ ...formData });\n    setLoaded(true);\n  }, [visible]);\n\n  return (\n    <Modal\n      show={visible}\n      onHide={() => onClose(false)}\n      className=\"w-100\"\n      dialogClassName=\"edit-post-modal\">\n      <Modal.Header closeButton>\n        <Modal.Title>\n          {t('edit_post', { keyPrefix: 'page_review' })}\n        </Modal.Title>\n      </Modal.Header>\n      <Form noValidate onSubmit={handleSubmit}>\n        <Modal.Body>\n          {objectType === 'question' && (\n            <Form.Group controlId=\"title\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.title.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.title.value}\n                isInvalid={formData.title.isInvalid}\n                onChange={(e) => {\n                  handleInput({\n                    title: {\n                      value: e.target.value,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}\n                placeholder={t('form.fields.title.placeholder')}\n                autoFocus\n                contentEditable\n              />\n\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.title.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n          )}\n\n          {objectType !== 'comment' && (\n            <Form.Group controlId=\"body\">\n              <Form.Label>\n                {objectType === 'question'\n                  ? t('form.fields.body.label')\n                  : t('form.fields.answer.label')}\n              </Form.Label>\n              <Form.Control\n                defaultValue={formData.content.value}\n                isInvalid={formData.content.isInvalid}\n                hidden\n              />\n              <Editor\n                value={formData.content.value}\n                onChange={(value) => {\n                  handleInput({\n                    content: { value, errorMsg: '', isInvalid: false },\n                  });\n                }}\n                className={classNames(\n                  'form-control p-0',\n                  focusEditor ? 'focus' : '',\n                )}\n                onFocus={() => {\n                  setFocusEditor(true);\n                }}\n                onBlur={() => {\n                  setFocusEditor(false);\n                }}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.content.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n          )}\n\n          {objectType === 'question' && (\n            <Form.Group controlId=\"tags\" className=\"my-3\">\n              <Form.Label>{t('form.fields.tags.label')}</Form.Label>\n              <TagSelector\n                value={formData.tags.value}\n                onChange={(value) => {\n                  handleInput({\n                    tags: { value, errorMsg: '', isInvalid: false },\n                  });\n                }}\n                showRequiredTag\n                maxTagLength={5}\n                isInvalid={formData.tags.isInvalid}\n                errMsg={formData.tags.errorMsg}\n              />\n            </Form.Group>\n          )}\n\n          {objectType === 'comment' && (\n            <div className=\"w-100\">\n              <div\n                className={classNames('custom-form-control', {\n                  'is-invalid': formData.content.isInvalid,\n                })}>\n                <Form.Label>Comment</Form.Label>\n                <Mentions\n                  pageUsers={pageUsers.getUsers()}\n                  onSelected={handleSelected}>\n                  <TextArea\n                    size=\"sm\"\n                    rows={4}\n                    value={parseEditMentionUser(formData.content.value)}\n                    onChange={(e) => {\n                      handleInput({\n                        content: {\n                          value: e.target.value,\n                          errorMsg: '',\n                          isInvalid: false,\n                        },\n                      });\n                    }}\n                  />\n                </Mentions>\n              </div>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.content.errorMsg}\n              </Form.Control.Feedback>\n            </div>\n          )}\n        </Modal.Body>\n        <Modal.Footer>\n          <Button variant=\"secondary\" onClick={() => onClose(false)}>\n            {t('close', { keyPrefix: 'btns' })}\n          </Button>\n          <Button variant=\"primary\" type=\"submit\">\n            {t('submit', { keyPrefix: 'btns' })}\n          </Button>\n        </Modal.Footer>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/FlagContent/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState, useRef } from 'react';\nimport { Card, Alert, Stack, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { getFlagReviewPostList, putFlagReviewAction } from '@/services';\nimport {\n  BaseUserCard,\n  Tag,\n  FormatTime,\n  ImgViewer,\n  htmlRender,\n} from '@/components';\nimport { scrollToDocTop } from '@/utils';\nimport type * as Type from '@/common/interface';\nimport { ADMIN_LIST_STATUS } from '@/common/constants';\nimport ApproveDropdown from '../ApproveDropdown';\nimport generateData from '../../utils/generateData';\n\ninterface IProps {\n  refreshCount: () => void;\n}\n\nconst Index: FC<IProps> = ({ refreshCount }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n  const ref = useRef<HTMLDivElement>(null);\n  const [noTasks, setNoTasks] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const [page, setPage] = useState(1);\n  const [reviewResp, setReviewResp] = useState<Type.FlagReviewResp>();\n  const flagItemData = reviewResp?.list[0] as Type.FlagReviewItem;\n\n  const resolveNextOne = (resp, pageNumber) => {\n    const { count, list = [] } = resp;\n    // auto rollback\n    if (!list.length && count && page !== 1) {\n      pageNumber = 1;\n      setPage(pageNumber);\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      queryNextOne(pageNumber);\n      return;\n    }\n    if (pageNumber !== page) {\n      setPage(pageNumber);\n    }\n    setReviewResp(resp);\n    if (!list.length) {\n      setNoTasks(true);\n    }\n    setTimeout(() => {\n      scrollToDocTop();\n    }, 150);\n  };\n\n  const queryNextOne = (pageNumber) => {\n    getFlagReviewPostList(pageNumber)\n      .then((resp) => {\n        resolveNextOne(resp, pageNumber);\n      })\n      .catch((ex) => {\n        console.error('review next error: ', ex);\n      });\n  };\n\n  useEffect(() => {\n    queryNextOne(page);\n  }, []);\n\n  const handlingApprove = () => {\n    if (!flagItemData) {\n      return;\n    }\n    refreshCount();\n    queryNextOne(page);\n  };\n\n  const handleIgnore = () => {\n    setIsLoading(true);\n    putFlagReviewAction({\n      operation_type: 'ignore_report',\n      flag_id: String(flagItemData?.flag_id),\n    })\n      .then(() => {\n        refreshCount();\n        queryNextOne(page);\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  const handlingSkip = () => {\n    queryNextOne(page + 1);\n  };\n\n  useEffect(() => {\n    if (!ref.current) {\n      return;\n    }\n\n    setTimeout(() => {\n      htmlRender(ref.current, {\n        copySuccessText: t('copied', { keyPrefix: 'messages' }),\n        copyText: t('copy', { keyPrefix: 'messages' }),\n      });\n    }, 70);\n  }, [ref.current]);\n\n  const {\n    object_type,\n    submitter_user,\n    author_user_info,\n    object_status,\n    reason,\n  } = flagItemData || {\n    object_type: '',\n    submitter_user: null,\n    author_user_info: null,\n    reason: null,\n    object_status: 0,\n  };\n\n  const { itemLink, itemId, itemTimePrefix } = generateData(flagItemData);\n\n  if (noTasks) return null;\n  return (\n    <Card>\n      <Card.Header>\n        {object_type !== 'user' ? t('flag_post') : t('flag_user')}\n      </Card.Header>\n      <Card.Body className=\"p-0\">\n        <Alert variant=\"info\" className=\"border-0 rounded-0 mb-0\">\n          <Stack\n            direction=\"horizontal\"\n            gap={1}\n            className=\"align-items-center mb-2\">\n            <BaseUserCard\n              data={submitter_user}\n              avatarSize=\"24px\"\n              avatarClass=\"me-2\"\n            />\n            {flagItemData?.submit_at && (\n              <FormatTime\n                time={flagItemData.submit_at}\n                className=\"small text-secondary\"\n                preFix={t('proposed')}\n              />\n            )}\n          </Stack>\n          <Stack className=\"align-items-start\">\n            <p className=\"mb-0\">\n              {object_type !== 'user'\n                ? t('flag_post_type', { type: reason?.name })\n                : t('flag_user_type', { type: reason?.name })}\n\n              {flagItemData?.reason_content &&\n                reason?.content_type &&\n                (reason?.reason_type !== 60 ? (\n                  <span> {flagItemData?.reason_content}</span>\n                ) : flagItemData.reason_content?.startsWith('http') ? (\n                  <a\n                    href={flagItemData.reason_content}\n                    target=\"_blank\"\n                    className=\"alert-exist\"\n                    rel=\"noreferrer\">\n                    <strong>\n                      {' '}\n                      {t('show_exist', { keyPrefix: 'question_detail' })}\n                    </strong>\n                  </a>\n                ) : (\n                  <strong> {flagItemData?.reason_content}</strong>\n                ))}\n            </p>\n          </Stack>\n        </Alert>\n        <div className=\"p-3\">\n          <small className=\"d-block text-secondary mb-4\">\n            <span>{t(object_type, { keyPrefix: 'btns' })} </span>\n            <Link to={itemLink} target=\"_blank\" className=\"link-secondary\">\n              #{itemId}\n            </Link>\n          </small>\n          {object_type === 'question' && (\n            <>\n              <h5 className=\"mb-3\">{flagItemData?.title}</h5>\n              <div className=\"mb-4\">\n                {flagItemData?.tags?.map((item) => {\n                  return (\n                    <Tag key={item.slug_name} className=\"me-1\" data={item} />\n                  );\n                })}\n              </div>\n            </>\n          )}\n          <div className=\"small font-monospace\">\n            <ImgViewer>\n              <article\n                ref={ref}\n                className=\"fmt text-break text-wrap\"\n                dangerouslySetInnerHTML={{ __html: flagItemData?.parsed_text }}\n              />\n            </ImgViewer>\n          </div>\n          <div className=\"d-flex flex-wrap align-items-center justify-content-between mt-4\">\n            <div>\n              <span\n                className={classNames(\n                  'badge',\n                  ADMIN_LIST_STATUS[object_status]?.variant,\n                )}>\n                {t(ADMIN_LIST_STATUS[object_status]?.name, {\n                  keyPrefix: 'btns',\n                })}\n              </span>\n              {flagItemData?.object_show_status === 2 && (\n                <span\n                  className={classNames(\n                    'ms-1 badge',\n                    ADMIN_LIST_STATUS.unlisted.variant,\n                  )}>\n                  {t(ADMIN_LIST_STATUS.unlisted.name, { keyPrefix: 'btns' })}\n                </span>\n              )}\n            </div>\n            <div className=\"d-flex align-items-center small\">\n              <BaseUserCard\n                data={author_user_info}\n                avatarSize=\"24px\"\n                avatarClass=\"me-2\"\n              />\n              <FormatTime\n                time={Number(flagItemData?.created_at)}\n                className=\"text-secondary ms-1 flex-shrink-0\"\n                preFix={t(itemTimePrefix, { keyPrefix: 'question_detail' })}\n              />\n            </div>\n          </div>\n        </div>\n      </Card.Body>\n\n      <Card.Footer className=\"p-3\">\n        <p>{t('approve_flag_tip')}</p>\n        <Stack direction=\"horizontal\" gap={2}>\n          <ApproveDropdown\n            objectType={object_type}\n            itemData={flagItemData}\n            curFilter={ADMIN_LIST_STATUS[object_status]?.name}\n            approveCallback={handlingApprove}\n          />\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handleIgnore}>\n            {t('ignore', { keyPrefix: 'btns' })}\n          </Button>\n\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handlingSkip}>\n            {t('skip', { keyPrefix: 'btns' })}\n          </Button>\n        </Stack>\n      </Card.Footer>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/QueuedContent/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState, useRef } from 'react';\nimport { Card, Alert, Stack, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link, useSearchParams } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { getPendingReviewPostList, putPendingReviewAction } from '@/services';\nimport {\n  BaseUserCard,\n  Tag,\n  FormatTime,\n  Icon,\n  ImgViewer,\n  htmlRender,\n} from '@/components';\nimport { scrollToDocTop } from '@/utils';\nimport type * as Type from '@/common/interface';\nimport { ADMIN_LIST_STATUS } from '@/common/constants';\nimport generateData from '../../utils/generateData';\n\ninterface IProps {\n  refreshCount: () => void;\n}\n\nconst Index: FC<IProps> = ({ refreshCount }) => {\n  const [urlSearch, setUrlSearchParams] = useSearchParams();\n  const objectId = urlSearch.get('objectId') || '';\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n  const ref = useRef<HTMLDivElement>(null);\n  const [noTasks, setNoTasks] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const [page, setPage] = useState(1);\n  const [reviewResp, setReviewResp] = useState<Type.QuestionDetailRes>();\n  const flagItemData = reviewResp?.list[0] as Type.QueuedReviewItem;\n\n  const resolveNextOne = (resp, pageNumber) => {\n    const { count, list = [] } = resp;\n    // auto rollback\n    if (!list.length && count && page !== 1) {\n      pageNumber = 1;\n      setPage(pageNumber);\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      queryNextOne(pageNumber, '');\n      return;\n    }\n    if (pageNumber !== page) {\n      setPage(pageNumber);\n    }\n    setReviewResp(resp);\n    if (!list.length) {\n      setNoTasks(true);\n    }\n    setTimeout(() => {\n      scrollToDocTop();\n    }, 150);\n  };\n\n  const queryNextOne = (pageNumber, id) => {\n    getPendingReviewPostList(pageNumber, id).then((resp) => {\n      resolveNextOne(resp, pageNumber);\n    });\n  };\n\n  useEffect(() => {\n    if (!ref.current) {\n      return;\n    }\n\n    setTimeout(() => {\n      htmlRender(ref.current, {\n        copySuccessText: t('copied', { keyPrefix: 'messages' }),\n        copyText: t('copy', { keyPrefix: 'messages' }),\n      });\n    }, 70);\n  }, [ref.current]);\n\n  useEffect(() => {\n    queryNextOne(page, objectId);\n  }, []);\n\n  const handleAction = (type: 'approve' | 'reject') => {\n    if (!flagItemData) {\n      return;\n    }\n    setIsLoading(true);\n    putPendingReviewAction({\n      status: type,\n      review_id: flagItemData?.review_id,\n    })\n      .then(() => {\n        refreshCount();\n        queryNextOne(page, '');\n        if (objectId) {\n          urlSearch.delete('objectId');\n          setUrlSearchParams(urlSearch);\n        }\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  const handlingSkip = () => {\n    queryNextOne(page + 1, '');\n    if (objectId) {\n      urlSearch.delete('objectId');\n      setUrlSearchParams(urlSearch);\n    }\n  };\n\n  const { object_type, author_user_info, object_status, reason } =\n    flagItemData || {\n      object_type: '',\n      author_user_info: null,\n      reason: null,\n      object_status: 0,\n    };\n\n  const { itemLink, itemId, itemTimePrefix } = generateData(flagItemData);\n\n  if (noTasks) return null;\n  return (\n    <Card>\n      <Card.Header>\n        {object_type !== 'user' ? t('queued_post') : t('queued_post_user')}\n      </Card.Header>\n      <Card.Body className=\"p-0\">\n        <Alert variant=\"info\" className=\"border-0 rounded-0 mb-0\">\n          <Stack\n            direction=\"horizontal\"\n            gap={1}\n            className=\"align-items-center mb-2\">\n            <div className=\"small d-flex align-items-center\">\n              <Icon type=\"bi\" name=\"plugin\" size=\"24px\" className=\"me-2 lh-1\" />\n              <span>{flagItemData?.submitter_display_name}</span>\n            </div>\n            {flagItemData?.submit_at && (\n              <FormatTime\n                time={flagItemData.submit_at}\n                className=\"small text-secondary\"\n                preFix={t('proposed')}\n              />\n            )}\n          </Stack>\n          <Stack className=\"align-items-start\">\n            <p className=\"mb-0\">{reason}</p>\n          </Stack>\n        </Alert>\n        <div className=\"p-3\">\n          <small className=\"d-block text-secondary mb-4\">\n            <span>{t(object_type, { keyPrefix: 'btns' })} </span>\n            <Link to={itemLink} target=\"_blank\" className=\"link-secondary\">\n              #{itemId}\n            </Link>\n          </small>\n          {object_type === 'question' && (\n            <>\n              <h5 className=\"mb-3\">{flagItemData?.title}</h5>\n              <div className=\"mb-4\">\n                {flagItemData?.tags?.map((item) => {\n                  return (\n                    <Tag key={item.slug_name} className=\"me-1\" data={item} />\n                  );\n                })}\n              </div>\n            </>\n          )}\n          <div className=\"small font-monospace\">\n            <ImgViewer>\n              <article\n                ref={ref}\n                className=\"fmt text-break text-wrap\"\n                dangerouslySetInnerHTML={{ __html: flagItemData?.parsed_text }}\n              />\n            </ImgViewer>\n          </div>\n          <div className=\"d-flex flex-wrap align-items-center justify-content-between mt-4\">\n            <div>\n              <span\n                className={classNames(\n                  'badge',\n                  ADMIN_LIST_STATUS[object_status]?.variant,\n                )}>\n                {t(ADMIN_LIST_STATUS[object_status]?.name, {\n                  keyPrefix: 'btns',\n                })}\n              </span>\n              {flagItemData?.object_show_status === 2 && (\n                <span\n                  className={classNames(\n                    'ms-1 badge',\n                    ADMIN_LIST_STATUS.unlisted.variant,\n                  )}>\n                  {t(ADMIN_LIST_STATUS.unlisted.name, { keyPrefix: 'btns' })}\n                </span>\n              )}\n            </div>\n            <div className=\"d-flex align-items-center small\">\n              <BaseUserCard\n                data={author_user_info}\n                avatarSize=\"24\"\n                avatarClass=\"me-2\"\n              />\n              <FormatTime\n                time={Number(flagItemData?.created_at)}\n                className=\"text-secondary ms-1 flex-shrink-0\"\n                preFix={t(itemTimePrefix, { keyPrefix: 'question_detail' })}\n              />\n            </div>\n          </div>\n        </div>\n      </Card.Body>\n\n      <Card.Footer className=\"p-3\">\n        <p>\n          {object_type !== 'user'\n            ? t('approve_post_tip')\n            : t('approve_user_tip')}\n        </p>\n        <Stack direction=\"horizontal\" gap={2}>\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={() => handleAction('approve')}>\n            {t('approve', { keyPrefix: 'btns' })}\n          </Button>\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={() => handleAction('reject')}>\n            {t('reject', { keyPrefix: 'btns' })}\n          </Button>\n\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handlingSkip}>\n            {t('skip', { keyPrefix: 'btns' })}\n          </Button>\n        </Stack>\n      </Card.Footer>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/ReviewType/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Card, Form } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport * as Type from '@/common/interface';\n\ninterface IProps {\n  list: Type.ReviewTypeItem[] | undefined;\n  checked: string;\n  callback: (type: string) => void;\n}\n\nconst Index: FC<IProps> = ({ list, checked, callback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n  return (\n    <Card>\n      <Card.Header>{t('filter', { keyPrefix: 'btns' })}</Card.Header>\n      <Card.Body>\n        <Form.Group>\n          <Form.Label>{t('filter_label')}</Form.Label>\n          {list?.map((item) => {\n            return (\n              <Form.Check\n                key={item.name}\n                type=\"radio\"\n                id={item.name}\n                disabled={item.todo_amount <= 0}\n                label={`${item.label} (${item.todo_amount})`}\n                checked={checked === item.name}\n                onChange={() => callback(item.name)}\n              />\n            );\n          })}\n        </Form.Group>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/SuggestContent/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Alert, Stack, Button, Card } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { BaseUserCard, FormatTime, DiffContent } from '@/components';\nimport { getSuggestReviewList, revisionAudit } from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\nimport { scrollToDocTop } from '@/utils';\nimport type * as Type from '@/common/interface';\n\ninterface IProps {\n  refreshCount: () => void;\n}\n\nconst Index: FC<IProps> = ({ refreshCount }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n  const [isLoading, setIsLoading] = useState(false);\n  const [noTasks, setNoTasks] = useState(false);\n  const [page, setPage] = useState(1);\n  const [reviewResp, setReviewResp] = useState<Type.SuggestReviewResp>();\n  const ro = reviewResp?.list[0];\n  const { info, type, unreviewed_info } = ro || {\n    info: null,\n    type: '',\n    unreviewed_info: null,\n  };\n  const resolveNextOne = (resp, pageNumber) => {\n    const { count, list = [] } = resp;\n    // auto rollback\n    if (!list.length && count && page !== 1) {\n      pageNumber = 1;\n      setPage(pageNumber);\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      queryNextOne(pageNumber);\n      return;\n    }\n    if (pageNumber !== page) {\n      setPage(pageNumber);\n    }\n    setReviewResp(resp);\n    if (!list.length) {\n      setNoTasks(true);\n    }\n    setTimeout(() => {\n      scrollToDocTop();\n    }, 150);\n  };\n  const queryNextOne = (pageNumber) => {\n    getSuggestReviewList(pageNumber).then((resp) => {\n      resolveNextOne(resp, pageNumber);\n    });\n  };\n  const reviewInfo = unreviewed_info?.content;\n\n  const handlingApprove = () => {\n    if (!unreviewed_info) {\n      return;\n    }\n    setIsLoading(true);\n    revisionAudit(unreviewed_info.id, 'approve')\n      .then(() => {\n        refreshCount();\n        queryNextOne(page);\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  const handlingReject = () => {\n    if (!unreviewed_info) {\n      return;\n    }\n    setIsLoading(true);\n    revisionAudit(unreviewed_info.id, 'reject')\n      .then(() => {\n        refreshCount();\n        queryNextOne(page);\n      })\n      .catch((ex) => {\n        console.error('revisionAudit reject error: ', ex);\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  const handlingSkip = () => {\n    queryNextOne(page + 1);\n  };\n\n  let itemLink = '';\n  let itemId = '';\n  let editSummary = unreviewed_info?.reason;\n  const editor = unreviewed_info?.user_info;\n  const editTime = unreviewed_info?.create_at;\n  if (type === 'question') {\n    itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);\n    itemId = info?.object_id;\n    editSummary ||= t('edit_question');\n  } else if (type === 'answer') {\n    itemLink = pathFactory.answerLanding({\n      // @ts-ignore\n      questionId: unreviewed_info.content.question_id,\n      slugTitle: info?.url_title,\n      answerId: unreviewed_info.object_id,\n    });\n    itemId = unreviewed_info.object_id;\n    editSummary ||= t('edit_answer');\n  } else if (type === 'tag') {\n    const tagInfo = unreviewed_info.content as Type.Tag;\n    itemLink = pathFactory.tagLanding(tagInfo.slug_name);\n    itemId = tagInfo?.tag_id || tagInfo.slug_name;\n    editSummary ||= t('edit_tag');\n  }\n  useEffect(() => {\n    queryNextOne(page);\n  }, []);\n\n  if (noTasks) return null;\n\n  let newData: Record<string, any> = {};\n  let oldData: Record<string, any> = {};\n  let diffOpts: Partial<{\n    showTitle: boolean;\n    showTagUrlSlug: boolean;\n  }> = {\n    showTitle: true,\n    showTagUrlSlug: true,\n  };\n  if (type === 'question' && info && reviewInfo && 'content' in reviewInfo) {\n    newData = {\n      title: reviewInfo.title,\n      original_text: reviewInfo.content,\n      tags: reviewInfo.tags,\n    };\n    oldData = {\n      title: info.title,\n      original_text: info.content,\n      tags: info.tags,\n    };\n  }\n  if (type === 'answer' && info && reviewInfo && 'content' in reviewInfo) {\n    newData = {\n      original_text: reviewInfo.content,\n    };\n    oldData = {\n      original_text: info.content,\n    };\n  }\n\n  if (type === 'tag' && info && reviewInfo) {\n    newData = {\n      original_text: reviewInfo.original_text,\n    };\n    oldData = {\n      original_text: info.content,\n    };\n    diffOpts = { showTitle: false, showTagUrlSlug: false };\n  }\n\n  return (\n    <Card>\n      <Card.Header>{t('suggest_edits')}</Card.Header>\n      <Card.Body className=\"p-0\">\n        <Alert variant=\"info\" className=\"border-0 rounded-0 mb-0\">\n          <Stack\n            direction=\"horizontal\"\n            gap={1}\n            className=\"align-items-center mb-2\">\n            <BaseUserCard data={editor} avatarSize=\"24\" avatarClass=\"me-2\" />\n            {editTime && (\n              <FormatTime\n                time={editTime}\n                className=\"small text-secondary\"\n                preFix={t('proposed')}\n              />\n            )}\n          </Stack>\n          <Stack className=\"align-items-start\">\n            <p className=\"mb-0\">{editSummary}</p>\n          </Stack>\n        </Alert>\n        <div className=\"p-3\">\n          <small className=\"d-block text-secondary mb-4\">\n            <span>{t(type, { keyPrefix: 'btns' })} </span>\n            <Link to={itemLink} target=\"_blank\" className=\"link-secondary\">\n              #{itemId}\n            </Link>\n          </small>\n\n          <DiffContent\n            className=\"mt-2\"\n            objectType={type}\n            newData={newData}\n            oldData={oldData}\n            opts={diffOpts}\n          />\n        </div>\n      </Card.Body>\n      <Card.Footer className=\"p-3\">\n        <p>{t('approve_revision_tip')}</p>\n        <Stack direction=\"horizontal\" gap={2}>\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handlingApprove}>\n            {t('approve', { keyPrefix: 'btns' })}\n          </Button>\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handlingReject}>\n            {t('reject', { keyPrefix: 'btns' })}\n          </Button>\n          <Button\n            variant=\"outline-primary\"\n            disabled={isLoading}\n            onClick={handlingSkip}>\n            {t('skip', { keyPrefix: 'btns' })}\n          </Button>\n        </Stack>\n      </Card.Footer>\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport ReviewType from './ReviewType';\nimport ApproveDropdown from './ApproveDropdown';\nimport EditPostModal from './EditPostModal';\nimport SuggestContent from './SuggestContent';\nimport FlagContent from './FlagContent';\nimport QueuedContent from './QueuedContent';\n\nexport {\n  ReviewType,\n  ApproveDropdown,\n  EditPostModal,\n  FlagContent,\n  SuggestContent,\n  QueuedContent,\n};\n"
  },
  {
    "path": "ui/src/pages/Review/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { Empty } from '@/components';\nimport { getReviewType } from '@/services';\nimport type * as Type from '@/common/interface';\n\nimport {\n  ReviewType,\n  FlagContent,\n  SuggestContent,\n  QueuedContent,\n} from './components';\n\nconst Index: FC = () => {\n  const [urlSearch, setUrlSearchParams] = useSearchParams();\n  const searchType = urlSearch.get('type');\n  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });\n  const [reviewTypeList, setReviewTypeList] = useState<Type.ReviewTypeItem[]>();\n  const [currentReviewType, setCurrentReviewType] = useState('');\n  const [isEmpty, setIsEmpty] = useState(false);\n\n  const fetchReviewType = (changeReviewType: boolean) => {\n    getReviewType()\n      .then((resp) => {\n        if (searchType) {\n          const filterData = resp.find((item) => item.name === searchType);\n          if (Number(filterData?.todo_amount) > 0) {\n            setCurrentReviewType(filterData?.name || '');\n          } else {\n            setIsEmpty(true);\n          }\n        } else {\n          const filterData = resp.filter((item) => item.todo_amount > 0);\n          if (filterData.length > 0) {\n            if (changeReviewType) {\n              setCurrentReviewType(filterData[0].name);\n            } else {\n              const currentTypeItem = resp.find(\n                (item) => item.name === currentReviewType,\n              );\n              if (currentTypeItem?.todo_amount === 0) {\n                setCurrentReviewType(filterData[0].name);\n              }\n            }\n          } else {\n            setIsEmpty(true);\n          }\n        }\n        setReviewTypeList(resp);\n      })\n      .catch((ex) => {\n        console.error('getReviewType error: ', ex);\n      });\n  };\n\n  const handleTypeChange = (name) => {\n    urlSearch.delete('type');\n    setUrlSearchParams(urlSearch);\n    setCurrentReviewType(name);\n  };\n\n  useEffect(() => {\n    fetchReviewType(true);\n  }, []);\n\n  usePageTags({\n    title: t('review'),\n  });\n\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{t('review')}</h3>\n      <Col className=\"page-main flex-auto\">\n        {currentReviewType === 'suggested_post_edit' && (\n          <SuggestContent refreshCount={() => fetchReviewType(false)} />\n        )}\n\n        {currentReviewType === 'flagged_post' && (\n          <FlagContent refreshCount={() => fetchReviewType(false)} />\n        )}\n\n        {currentReviewType === 'queued_post' && (\n          <QueuedContent refreshCount={() => fetchReviewType(false)} />\n        )}\n        {isEmpty && <Empty>{t('empty')}</Empty>}\n      </Col>\n\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <ReviewType\n          list={reviewTypeList}\n          checked={currentReviewType}\n          callback={handleTypeChange}\n        />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Review/utils/generateData.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { pathFactory } from '@/router/pathFactory';\n\nexport default (data: any) => {\n  if (!data?.object_id) {\n    return {\n      itemLink: '',\n      itemId: '',\n      itemTimePrefix: '',\n    };\n  }\n\n  const {\n    object_type = '',\n    object_id = '',\n    question_id = '',\n    answer_id = '',\n    comment_id = '',\n    url_title = '',\n  } = data;\n  let itemLink = '';\n  let itemId = '';\n  let itemTimePrefix = '';\n\n  if (object_type === 'question') {\n    itemLink = pathFactory.questionLanding(String(object_id), url_title);\n    itemId = String(question_id);\n    itemTimePrefix = 'asked';\n  } else if (object_type === 'answer') {\n    itemLink = pathFactory.answerLanding({\n      // @ts-ignore\n      questionId: question_id,\n      slugTitle: url_title,\n      answerId: String(object_id),\n    });\n    itemId = String(object_id);\n    itemTimePrefix = 'answered';\n  } else if (object_type === 'comment') {\n    if (question_id && answer_id) {\n      itemLink = `${pathFactory.answerLanding({\n        questionId: question_id,\n        slugTitle: url_title,\n        answerId: answer_id,\n      })}?commentId=${comment_id}`;\n    } else {\n      itemLink = `${pathFactory.questionLanding(\n        String(question_id),\n        url_title,\n      )}?commentId=${comment_id}`;\n    }\n    itemId = String(comment_id);\n    itemTimePrefix = 'commented';\n  }\n\n  return {\n    itemLink,\n    itemId,\n    itemTimePrefix,\n  };\n};\n"
  },
  {
    "path": "ui/src/pages/Search/components/AiCard/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { Card, Spinner } from 'react-bootstrap';\nimport { useSearchParams, Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { v4 as uuidv4 } from 'uuid';\n\nimport { BubbleAi, BubbleUser } from '@/components';\nimport { aiControlStore } from '@/stores';\nimport * as Type from '@/common/interface';\nimport requestAi from '@/utils/requestAi';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });\n  const { ai_enabled } = aiControlStore((state) => state);\n  const [searchParams] = useSearchParams();\n  const [isLoading, setIsLoading] = useState(false);\n  const [isGenerate, setIsGenerate] = useState(false);\n  const [isCompleted, setIsCompleted] = useState(false);\n  const [conversions, setConversions] = useState<Type.ConversationDetail>({\n    records: [],\n    conversation_id: '',\n    created_at: 0,\n    topic: '',\n    updated_at: 0,\n  });\n\n  const handleSubmit = async (userMsg) => {\n    setIsLoading(true);\n    setIsCompleted(false);\n    const newConversationId = uuidv4();\n    setConversions({\n      conversation_id: newConversationId,\n      created_at: 0,\n      topic: '',\n      updated_at: 0,\n      records: [\n        {\n          chat_completion_id: Date.now().toString(),\n          role: 'user',\n          content: userMsg,\n          helpful: 0,\n          unhelpful: 0,\n          created_at: Date.now(),\n        },\n      ],\n    });\n\n    const params = {\n      conversation_id: newConversationId,\n      messages: [\n        {\n          role: 'user',\n          content: userMsg,\n        },\n      ],\n    };\n\n    await requestAi('/answer/api/v1/chat/completions', {\n      body: JSON.stringify(params),\n      onMessage: (res) => {\n        if (!res.choices[0].delta?.content) {\n          return;\n        }\n        setIsLoading(false);\n        setIsGenerate(true);\n\n        setConversions((prev) => {\n          const updatedRecords = [...prev.records];\n          const lastConversion = updatedRecords[updatedRecords.length - 1];\n          if (lastConversion?.chat_completion_id === res?.chat_completion_id) {\n            updatedRecords[updatedRecords.length - 1] = {\n              ...lastConversion,\n              content: lastConversion.content + res.choices[0].delta.content,\n            };\n          } else {\n            updatedRecords.push({\n              chat_completion_id: res.chat_completion_id,\n              role: res.choices[0].delta.role || 'assistant',\n              content: res.choices[0].delta.content,\n              helpful: 0,\n              unhelpful: 0,\n              created_at: Date.now(),\n            });\n          }\n          return {\n            ...prev,\n            conversation_id: params.conversation_id,\n            records: updatedRecords,\n          };\n        });\n      },\n      onError: (error) => {\n        setIsGenerate(false);\n        setIsLoading(false);\n        setIsCompleted(true);\n        console.error('Error:', error);\n      },\n      onComplete: () => {\n        setIsCompleted(true);\n        setIsGenerate(false);\n      },\n    });\n  };\n\n  useEffect(() => {\n    const q = searchParams.get('q') || '';\n    if (ai_enabled && q) {\n      handleSubmit(q);\n    }\n  }, [searchParams]);\n\n  if (!ai_enabled) {\n    return null;\n  }\n  return (\n    <Card className=\"mb-5\">\n      <Card.Header>\n        {t('ai_assistant', { keyPrefix: 'page_title' })}\n      </Card.Header>\n      <Card.Body>\n        {conversions?.records.map((item, index) => {\n          const isLastMessage =\n            index === Number(conversions?.records.length) - 1;\n          return (\n            <div\n              key={`${item.chat_completion_id}-${item.role}`}\n              className={`${isLastMessage ? '' : 'mb-4'}`}>\n              {item.role === 'user' ? (\n                <BubbleUser content={item.content} />\n              ) : (\n                <BubbleAi\n                  canType\n                  chatId={item.chat_completion_id}\n                  isLast\n                  isCompleted={!isGenerate}\n                  content={item.content}\n                  actionData={{\n                    helpful: item.helpful,\n                    unhelpful: item.unhelpful,\n                  }}\n                />\n              )}\n            </div>\n          );\n        })}\n        {isLoading && (\n          <Spinner\n            animation=\"border\"\n            size=\"sm\"\n            variant=\"secondary\"\n            className=\"mt-4\"\n          />\n        )}\n      </Card.Body>\n      {isCompleted && !isLoading && (\n        <Card.Footer className=\"py-3\">\n          <Link\n            className=\"btn btn-outline-primary me-3\"\n            to={`/ai-assistant/${conversions.conversation_id}`}>\n            {t('ask_a_follow_up')}\n          </Link>\n          <span className=\"small text-secondary\">{t('ai_generate')}</span>\n        </Card.Footer>\n      )}\n    </Card>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Search/components/Empty/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Trans } from 'react-i18next';\n\nconst Index: FC = () => {\n  return (\n    <div className=\"mt-5 text-center\">\n      <Trans i18nKey=\"search.empty\">\n        We couldn't find anything.\n        <br />\n        Try different or less specific keywords.\n      </Trans>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/Head/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState } from 'react';\nimport { useSearchParams, Link } from 'react-router-dom';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { following } from '@/services';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { escapeRemove } from '@/utils';\nimport { pathFactory } from '@/router/pathFactory';\nimport { PluginRender } from '@/components';\nimport Pattern from '@/common/pattern';\nimport { PluginType } from '@/utils/pluginKit';\n\ninterface Props {\n  data;\n}\n\nconst Index: FC<Props> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'search' });\n  const [searchParams] = useSearchParams();\n  const q = searchParams.get('q');\n  const options = q?.match(Pattern.search);\n  const [followed, setFollowed] = useState(data?.is_follower);\n\n  const follow = () => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    following({\n      object_id: data?.tag_id,\n      is_cancel: followed,\n    }).then((res) => {\n      setFollowed(res.is_followed);\n    });\n  };\n\n  return (\n    <div className=\"mb-5\">\n      <div className=\"mb-3 d-flex align-items-center justify-content-between\">\n        <h3 className=\"mb-0\">{t('title')}</h3>\n\n        <PluginRender type={PluginType.Search} slug_name=\"serarch_info\" />\n      </div>\n      <p>\n        <span className=\"text-secondary me-1\">{t('keywords')}</span>\n        {q?.replace(Pattern.search, '')}\n        <br />\n        {options?.length && (\n          <>\n            <span className=\"text-secondary\">{t('options')} </span>\n            {options?.map((item) => {\n              return <code key={item}>{item} </code>;\n            })}\n          </>\n        )}\n      </p>\n      {data?.slug_name && (\n        <>\n          {data.excerpt && (\n            <p className=\"text-break\">\n              {escapeRemove(data.excerpt)}\n              <Link className=\"ms-1\" to={pathFactory.tagInfo(data.slug_name)}>\n                [{t('more')}]\n              </Link>\n            </p>\n          )}\n\n          <Button variant=\"outline-primary\" onClick={follow}>\n            {followed ? t('following') : t('follow')}\n          </Button>\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/ListLoader/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroupItem } from 'react-bootstrap';\n\ninterface Props {\n  count?: number;\n}\n\nconst Index: FC<Props> = ({ count = 10 }) => {\n  const list = new Array(count).fill(0).map((v, i) => v + i);\n  return (\n    <>\n      {list.map((v) => (\n        <ListGroupItem\n          className=\"py-3 px-0 border-start-0 border-end-0 bg-transparent placeholder-glow\"\n          key={v}>\n          <div className=\"mb-2\">\n            <div\n              className=\"placeholder me-2\"\n              style={{ height: '25px', width: '30px' }}\n            />\n            <div\n              className=\"h5 mb-0 w-75 placeholder\"\n              style={{ height: '25px' }}\n            />\n          </div>\n          <div\n            className=\"placeholder w-50 h5 align-top mb-2\"\n            style={{ height: '21px' }}\n          />\n\n          <div\n            className=\"placeholder w-100 d-block align-top mb-2\"\n            style={{ height: '42px' }}\n          />\n\n          <div\n            className=\"placeholder w-25 align-top\"\n            style={{ height: '24px' }}\n          />\n        </ListGroupItem>\n      ))}\n    </>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/SearchHead/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { QueryGroup } from '@/components';\n\nconst sortBtns = ['relevance', 'newest', 'active', 'score'];\n\ninterface Props {\n  count: number;\n  sort: string;\n}\nconst Index: FC<Props> = ({ sort, count = 0 }) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'search.sort_btns',\n  });\n\n  return (\n    <div className=\"d-flex flex-wrap align-items-center justify-content-between pt-2 pb-3\">\n      <h5 className=\"mb-0\">\n        {count === -1\n          ? t('counts_loading', { keyPrefix: 'search' })\n          : t('counts', { count, keyPrefix: 'search' })}\n      </h5>\n      <QueryGroup\n        data={sortBtns}\n        currentSort={sort}\n        sortKey=\"order\"\n        i18nKeyPrefix=\"search.sort_btns\"\n      />\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/SearchItem/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { ListGroupItem } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport {\n  Tag,\n  FormatTime,\n  BaseUserCard,\n  Counts,\n  HighlightText,\n} from '@/components';\nimport Pattern from '@/common/pattern';\nimport type { SearchResItem } from '@/common/interface';\nimport { escapeRemove } from '@/utils';\n\ninterface Props {\n  data: SearchResItem;\n}\nconst Index: FC<Props> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'question' });\n  if (!data?.object_type) {\n    return null;\n  }\n  let itemUrl = pathFactory.questionLanding(\n    data.object.id,\n    data.object.url_title,\n  );\n  if (data.object_type === 'answer' && data.object.question_id) {\n    itemUrl = pathFactory.answerLanding({\n      questionId: data.object.question_id,\n      slugTitle: data.object.url_title,\n      answerId: data.object.id,\n    });\n  }\n\n  const [searchParams] = useSearchParams();\n  const q = searchParams.get('q');\n  const keywords =\n    q\n      ?.replace(Pattern.search, '')\n      ?.split(' ')\n      ?.filter((v) => v !== '') || [];\n\n  return (\n    <ListGroupItem className=\"py-3 px-0 border-start-0 border-end-0 bg-transparent\">\n      <div className=\"mb-2 clearfix\">\n        <span\n          className=\"float-start me-2 badge text-bg-dark\"\n          style={{ marginTop: '2px' }}>\n          {t(data.object_type, { keyPrefix: 'btns' })}\n        </span>\n        <Link className=\"h5 mb-0 link-dark text-break\" to={itemUrl}>\n          <HighlightText text={data.object.title} keywords={keywords} />\n          {data.object.status === 'closed'\n            ? ` [${t('closed', { keyPrefix: 'question' })}]`\n            : null}\n        </Link>\n      </div>\n      <div className=\"d-flex flex-wrap align-items-center small text-secondary mb-2\">\n        <BaseUserCard data={data.object?.user_info} showAvatar={false} />\n\n        <span className=\"split-dot\" />\n        <FormatTime time={data.object?.created_at} className=\"me-3\" />\n\n        <Counts\n          className=\"my-2 my-sm-0\"\n          showViews={false}\n          isAccepted={data.object?.accepted}\n          showAnswers={data.object_type === 'question'}\n          showAccepted={data.object?.accepted && data.object_type === 'answer'}\n          data={{\n            votes: data.object?.vote_count,\n            answers: data.object?.answer_count,\n            views: 0,\n          }}\n        />\n      </div>\n\n      {data.object?.excerpt && (\n        <p className=\"small text-truncate-2 mb-2 last-p text-break\">\n          <HighlightText\n            text={escapeRemove(data.object.excerpt) || ''}\n            keywords={keywords}\n          />\n        </p>\n      )}\n\n      {data.object?.tags?.map((item) => {\n        return <Tag key={item.slug_name} className=\"me-1\" data={item} />;\n      })}\n    </ListGroupItem>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/Tips/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Card } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\n\nconst Index: FC = () => {\n  const { t } = useTranslation();\n  return (\n    <Card>\n      <Card.Header>{t('search.tips.title')}</Card.Header>\n      <Card.Body>\n        <div className=\"mb-1\">\n          <Trans i18nKey=\"search.tips.tag\" components={{ 1: <code /> }} />\n        </div>\n        <div className=\"mb-1\">\n          <Trans i18nKey=\"search.tips.user\" components={{ 1: <code /> }} />\n        </div>\n        <div className=\"mb-1\">\n          <Trans i18nKey=\"search.tips.answer\" components={{ 1: <code /> }} />\n        </div>\n        <div className=\"mb-1\">\n          <Trans i18nKey=\"search.tips.score\" components={{ 1: <code /> }} />\n        </div>\n        <div className=\"mb-1\">\n          <Trans i18nKey=\"search.tips.question\" components={{ 1: <code /> }} />\n        </div>\n        <div>\n          <Trans i18nKey=\"search.tips.is_answer\" components={{ 1: <code /> }} />\n        </div>\n      </Card.Body>\n    </Card>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Search/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Head from './Head';\nimport SearchItem from './SearchItem';\nimport Tips from './Tips';\nimport Empty from './Empty';\nimport SearchHead from './SearchHead';\nimport ListLoader from './ListLoader';\nimport AiCard from './AiCard';\n\nexport { Head, SearchItem, Tips, Empty, SearchHead, ListLoader, AiCard };\n"
  },
  {
    "path": "ui/src/pages/Search/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Row, Col, ListGroup } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams } from 'react-router-dom';\nimport { useEffect, useState } from 'react';\n\nimport { usePageTags, useSkeletonControl } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { Pagination } from '@/components';\nimport { getSearchResult } from '@/services';\nimport type { SearchParams, SearchRes } from '@/common/interface';\nimport { logged } from '@/utils/guard';\n\nimport {\n  Head,\n  SearchHead,\n  SearchItem,\n  Tips,\n  Empty,\n  ListLoader,\n  AiCard,\n} from './components';\n\nconst Index = () => {\n  const { t } = useTranslation('translation');\n  const isLogged = logged().ok;\n  const [searchParams] = useSearchParams();\n  const page = searchParams.get('page') || 1;\n  const q = searchParams.get('q') || '';\n  const order = searchParams.get('order') || 'relevance';\n  const [isLoading, setIsLoading] = useState(false);\n  const { isSkeletonShow } = useSkeletonControl(isLoading);\n  const [data, setData] = useState<SearchRes>({\n    count: 0,\n    list: [],\n    extra: null,\n  });\n  const { count = 0, list = [], extra = null } = data || {};\n\n  const searchCaptcha = useCaptchaPlugin('search');\n\n  const doSearch = () => {\n    setIsLoading(true);\n    const params: SearchParams = {\n      q,\n      order,\n      page: Number(page),\n      size: 20,\n    };\n\n    const captcha = searchCaptcha?.getCaptcha();\n    if (captcha?.verify) {\n      params.captcha_id = captcha.captcha_id;\n      params.captcha_code = captcha.captcha_code;\n    }\n\n    getSearchResult(params)\n      .then(async (resp) => {\n        await searchCaptcha?.close();\n        setData(resp);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          searchCaptcha?.handleCaptchaError(err.list);\n        }\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  useEffect(() => {\n    if (!searchCaptcha) {\n      doSearch();\n      return;\n    }\n    searchCaptcha.check(() => {\n      doSearch();\n    });\n  }, [q, order, page]);\n\n  let pageTitle = t('search', { keyPrefix: 'page_title' });\n  if (q) {\n    pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;\n  }\n  usePageTags({\n    title: pageTitle,\n  });\n\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col className=\"page-main flex-auto\">\n        <Head data={extra} />\n        {isLogged && <AiCard />}\n        <SearchHead sort={order} count={isLoading ? -1 : count} />\n        <ListGroup className=\"rounded-0 mb-5\">\n          {isSkeletonShow ? (\n            <ListLoader />\n          ) : (\n            list?.map((item) => {\n              return <SearchItem key={item.object.id} data={item} />;\n            })\n          )}\n        </ListGroup>\n\n        {!isLoading && !list?.length && <Empty />}\n\n        <div className=\"d-flex justify-content-center\">\n          <Pagination\n            currentPage={Number(page)}\n            pageSize={20}\n            totalSize={count}\n          />\n        </div>\n      </Col>\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <Tips />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/SideNavLayout/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Outlet } from 'react-router-dom';\n\nimport { SideNav, Footer } from '@/components';\n\nimport '@/common/sideNavLayout.scss';\n\nconst Index: FC = () => {\n  return (\n    <div className=\"d-flex\">\n      <div\n        className=\"position-sticky px-3 border-end pt-4 d-none d-lg-block\"\n        id=\"pcSideNav\">\n        <SideNav />\n      </div>\n      <div className=\"flex-fill w-100 overflow-x-hidden\">\n        <div className=\"d-flex justify-content-center px-0 px-md-4\">\n          <div className=\"answer-container main-mx-with\">\n            <Outlet />\n          </div>\n        </div>\n        <div className=\"d-flex justify-content-center px-0 px-md-4\">\n          <div className=\"main-mx-with\">\n            <Footer />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/SideNavLayoutWithoutFooter/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Outlet } from 'react-router-dom';\n\nimport { SideNav } from '@/components';\n\nimport '@/common/sideNavLayout.scss';\n\nconst Index: FC = () => {\n  return (\n    <div className=\"d-flex flex-fill\">\n      <div\n        className=\"position-sticky px-3 border-end py-4 d-none d-xl-block\"\n        id=\"pcSideNav\">\n        <SideNav />\n      </div>\n      <div className=\"flex-fill w-100 d-flex flex-column\">\n        <div className=\"d-flex justify-content-center flex-grow-1 px-0 px-md-4\">\n          <div className=\"d-flex flex-column flex-1 main-mx-with\">\n            <Outlet />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Tags/Create/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { Row, Col, Form, Button, Card } from 'react-bootstrap';\nimport { useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { usePageTags, usePromptWithUnload } from '@/hooks';\nimport { Editor, EditorRef } from '@/components';\nimport { loggedUserInfoStore } from '@/stores';\nimport type * as Type from '@/common/interface';\nimport { createTag } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { TAG_SLUG_NAME_MAX_LENGTH } from '@/common/constants';\n\ninterface FormDataItem {\n  displayName: Type.FormValue<string>;\n  slugName: Type.FormValue<string>;\n  description: Type.FormValue<string>;\n}\n\nconst Index = () => {\n  const initFormData = {\n    displayName: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    slugName: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    description: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  };\n  const { role_id = 1 } = loggedUserInfoStore((state) => state.user);\n  const navigate = useNavigate();\n  const { t } = useTranslation('translation', { keyPrefix: 'tag_modal' });\n  const [focusType, setForceType] = useState('');\n\n  const [formData, setFormData] = useState<FormDataItem>(initFormData);\n  const [immData] = useState(initFormData);\n  const [contentChanged, setContentChanged] = useState(false);\n\n  const editorRef = useRef<EditorRef>({\n    getHtml: () => '',\n  });\n\n  usePromptWithUnload({\n    when: contentChanged,\n  });\n\n  useEffect(() => {\n    const { displayName, slugName, description } = formData;\n    const {\n      displayName: display_name,\n      slugName: slug_name,\n      description: original_text,\n    } = immData;\n    if (!display_name || !slug_name || !original_text) {\n      return;\n    }\n\n    if (\n      display_name.value !== displayName.value ||\n      slug_name.value !== slugName.value ||\n      original_text.value !== description.value\n    ) {\n      setContentChanged(true);\n    } else {\n      setContentChanged(false);\n    }\n  }, [\n    formData.displayName.value,\n    formData.slugName.value,\n    formData.description.value,\n  ]);\n\n  const handleDescriptionChange = (value: string) =>\n    setFormData((prev) => ({\n      ...prev,\n      description: { value, isInvalid: false, errorMsg: '' },\n    }));\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    let errObjKey = '';\n    const { displayName, slugName } = formData;\n\n    if (!displayName.value) {\n      bol = false;\n      errObjKey = 'display_name';\n      formData.displayName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.empty'),\n      };\n    } else if (displayName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      errObjKey = 'display_name';\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.range'),\n      };\n    } else {\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    if (!slugName.value) {\n      bol = false;\n      errObjKey = 'slug_name';\n      formData.slugName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.empty'),\n      };\n    } else if (slugName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      errObjKey = 'slug_name';\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.range'),\n      };\n    } else {\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n\n    if (!bol) {\n      const ele = document.getElementById(errObjKey);\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n    setContentChanged(false);\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    const params = {\n      display_name: formData.displayName.value,\n      slug_name: formData.slugName.value,\n      original_text: formData.description.value,\n    };\n    createTag(params)\n      .then((res) => {\n        navigate(`/tags/${encodeURIComponent(res.slug_name)}/info`, {\n          replace: true,\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData, [\n            { from: 'display_name', to: 'displayName' },\n            { from: 'slug_name', to: 'slugName' },\n            { from: 'original_text', to: 'description' },\n          ]);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      displayName: {\n        ...formData.displayName,\n        value: e.currentTarget.value,\n        isInvalid: false,\n      },\n    });\n  };\n\n  const handleSlugNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      slugName: {\n        ...formData.slugName,\n        value: e.currentTarget.value,\n        isInvalid: false,\n      },\n    });\n  };\n\n  usePageTags({\n    title: t('create_tag', { keyPrefix: 'page_title' }),\n  });\n\n  return (\n    <div className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <Row>\n        <Col className=\"page-main flex-auto\">\n          <Form noValidate onSubmit={handleSubmit}>\n            <Form.Group controlId=\"display_name\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.display_name.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.displayName.value}\n                isInvalid={formData.displayName.isInvalid}\n                disabled={role_id !== 2 && role_id !== 3}\n                onChange={handleDisplayNameChange}\n              />\n\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.displayName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"slug_name\" className=\"mb-3\">\n              <Form.Label>{t('form.fields.slug_name.label')}</Form.Label>\n              <Form.Control\n                type=\"text\"\n                value={formData.slugName.value}\n                isInvalid={formData.slugName.isInvalid}\n                disabled={role_id !== 2 && role_id !== 3}\n                onChange={handleSlugNameChange}\n              />\n              <Form.Text as=\"div\">{t('form.fields.slug_name.desc')}</Form.Text>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.slugName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <Form.Group controlId=\"description\" className=\"mt-4\">\n              <Form.Label>{t('form.fields.desc.label')}</Form.Label>\n              <Editor\n                value={formData.description.value}\n                onChange={handleDescriptionChange}\n                className={classNames(\n                  'form-control p-0',\n                  focusType === 'description' && 'focus',\n                )}\n                onFocus={() => {\n                  setForceType('description');\n                }}\n                onBlur={() => {\n                  setForceType('');\n                }}\n                ref={editorRef}\n              />\n              <Form.Control\n                value={formData.description.value}\n                type=\"text\"\n                isInvalid={formData.description.isInvalid}\n                readOnly\n                hidden\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.description.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <div className=\"mt-3\">\n              <Button type=\"submit\">{t('btn_post')}</Button>\n            </div>\n          </Form>\n        </Col>\n        <Col className=\"page-right-side mt-4 mt-xl-0\">\n          <Card>\n            <Card.Header>\n              {t('title', { keyPrefix: 'how_to_format' })}\n            </Card.Header>\n            <Card.Body\n              className=\"fmt small\"\n              dangerouslySetInnerHTML={{\n                __html: t('desc', { keyPrefix: 'how_to_format' }),\n              }}\n            />\n          </Card>\n        </Col>\n      </Row>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Tags/Detail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport { Row, Col, Button } from 'react-bootstrap';\nimport {\n  useParams,\n  Link,\n  useNavigate,\n  useSearchParams,\n} from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport * as Type from '@/common/interface';\nimport { FollowingTags, CustomSidebar, Icon } from '@/components';\nimport {\n  useTagInfo,\n  useFollow,\n  useQuerySynonymsTags,\n  useQuestionList,\n} from '@/services';\nimport QuestionList, { QUESTION_ORDER_KEYS } from '@/components/QuestionList';\nimport HotQuestions from '@/components/HotQuestions';\nimport { guard, pageTitleType } from '@/utils';\nimport { pathFactory } from '@/router/pathFactory';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'tags' });\n  const navigate = useNavigate();\n  const routeParams = useParams();\n  const curTagName = routeParams.tagName || '';\n  const [urlSearchParams] = useSearchParams();\n  const curOrder = (urlSearchParams.get('order') ||\n    QUESTION_ORDER_KEYS[0]) as Type.QuestionOrderBy;\n  const curPage = Number(urlSearchParams.get('page')) || 1;\n  const reqParams: Type.QueryQuestionsReq = {\n    page_size: 20,\n    page: curPage,\n    order: curOrder as Type.QuestionOrderBy,\n    tag: routeParams.tagName,\n  };\n  const [tagInfo, setTagInfo] = useState<any>({});\n  const [tagFollow, setTagFollow] = useState<Type.FollowParams>();\n  const { data: tagResp, isLoading } = useTagInfo({ name: curTagName });\n  const { data: listData, isLoading: listLoading } = useQuestionList(reqParams);\n  const { data: followResp } = useFollow(tagFollow);\n  const { data: synonymsRes } = useQuerySynonymsTags(\n    tagInfo?.tag_id,\n    tagInfo?.status,\n  );\n  const toggleFollow = () => {\n    if (!guard.tryNormalLogged(true)) {\n      return;\n    }\n    setTagFollow({\n      is_cancel: tagInfo.is_follower,\n      object_id: tagInfo.tag_id,\n    });\n  };\n\n  useEffect(() => {\n    if (tagResp) {\n      const info = { ...tagResp };\n      if (info.main_tag_slug_name) {\n        navigate(pathFactory.tagLanding(info.main_tag_slug_name), {\n          replace: true,\n        });\n        return;\n      }\n      if (followResp) {\n        info.is_follower = followResp.is_followed;\n      }\n\n      if (info.excerpt) {\n        info.excerpt =\n          info.excerpt.length > 256\n            ? [...info.excerpt].slice(0, 256).join('')\n            : info.excerpt;\n      }\n\n      setTagInfo(info);\n    }\n  }, [tagResp, followResp]);\n  let pageTitle = '';\n  if (tagInfo?.display_name) {\n    pageTitle = `'${tagInfo.display_name}' ${t(pageTitleType(), {\n      keyPrefix: 'page_title',\n    })}`;\n  }\n  const keywords: string[] = [];\n  if (tagInfo?.slug_name) {\n    keywords.push(tagInfo.slug_name);\n  }\n  synonymsRes?.synonyms.forEach((o) => {\n    keywords.push(o.slug_name);\n  });\n  usePageTags({\n    title: pageTitle,\n    description: tagInfo?.description,\n    keywords: keywords.join(','),\n  });\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col className=\"page-main flex-auto\">\n        {isLoading ? (\n          <div className=\"tag-box mb-5 placeholder-glow\">\n            <div className=\"mb-3 h3 placeholder\" style={{ width: '120px' }} />\n            <p\n              className=\"placeholder w-100 d-block align-top\"\n              style={{ height: '24px' }}\n            />\n\n            <div\n              className=\"placeholder d-block align-top\"\n              style={{ height: '38px', width: '100px' }}\n            />\n          </div>\n        ) : (\n          <div className=\"tag-box mb-5\">\n            <h3 className=\"mb-3\">\n              <Link\n                to={pathFactory.tagLanding(tagInfo.slug_name)}\n                replace\n                className=\"link-dark\">\n                {tagInfo.display_name}\n              </Link>\n            </h3>\n\n            <div\n              className=\"text-break\"\n              dangerouslySetInnerHTML={{ __html: tagInfo.excerpt }}\n            />\n\n            <div className=\"box-ft\">\n              {tagInfo.is_follower ? (\n                <div>\n                  <Button variant=\"primary\" onClick={() => toggleFollow()}>\n                    {t('button_following')}\n                  </Button>\n                  <Link\n                    to={pathFactory.tagInfo(curTagName)}\n                    className=\"btn btn-outline-secondary ms-2\">\n                    {t('wiki')}\n                  </Link>\n                  <Link\n                    className=\"btn btn-outline-secondary ms-2\"\n                    to=\"/users/settings/notify\">\n                    <Icon name=\"bell-fill\" />\n                  </Link>\n                </div>\n              ) : (\n                <div>\n                  <Button\n                    variant=\"outline-primary\"\n                    onClick={() => toggleFollow()}>\n                    {t('button_follow')}\n                  </Button>\n                  <Link\n                    to={pathFactory.tagInfo(curTagName)}\n                    className=\"btn btn-outline-secondary ms-2\">\n                    {t('wiki')}\n                  </Link>\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n        <QuestionList\n          source=\"tag\"\n          data={listData}\n          order={curOrder}\n          orderList={QUESTION_ORDER_KEYS.filter((k) => k !== 'recommend')}\n          isLoading={listLoading}\n        />\n      </Col>\n      <Col className=\"page-right-side mt-4 mt-xl-0\">\n        <CustomSidebar />\n        <FollowingTags />\n        <HotQuestions />\n      </Col>\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Tags/Edit/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { Row, Col, Form, Button, Card } from 'react-bootstrap';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport dayjs from 'dayjs';\nimport classNames from 'classnames';\n\nimport { usePageTags, usePromptWithUnload } from '@/hooks';\nimport { Editor, EditorRef } from '@/components';\nimport { loggedUserInfoStore } from '@/stores';\nimport type * as Type from '@/common/interface';\nimport { TAG_SLUG_NAME_MAX_LENGTH } from '@/common/constants';\nimport { useTagInfo, modifyTag, useQueryRevisions } from '@/services';\n\ninterface FormDataItem {\n  displayName: Type.FormValue<string>;\n  slugName: Type.FormValue<string>;\n  description: Type.FormValue<string>;\n  editSummary: Type.FormValue<string>;\n}\nconst initFormData = {\n  displayName: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n  slugName: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n  description: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n  editSummary: {\n    value: '',\n    isInvalid: false,\n    errorMsg: '',\n  },\n};\n\nconst Index = () => {\n  const { role_id = 1 } = loggedUserInfoStore((state) => state.user);\n\n  const { tagId } = useParams();\n  const navigate = useNavigate();\n  const { t } = useTranslation('translation', { keyPrefix: 'edit_tag' });\n  const [focusType, setForceType] = useState('');\n\n  const { data } = useTagInfo({ id: tagId });\n  const { data: revisions = [] } = useQueryRevisions(data?.tag_id);\n  const [formData, setFormData] = useState<FormDataItem>(initFormData);\n  const [immData, setImmData] = useState(initFormData);\n  const [contentChanged, setContentChanged] = useState(false);\n\n  const editorRef = useRef<EditorRef>({\n    getHtml: () => '',\n  });\n\n  usePromptWithUnload({\n    when: contentChanged,\n  });\n\n  useEffect(() => {\n    initFormData.displayName.value = data?.display_name || '';\n    initFormData.slugName.value = data?.slug_name || '';\n    initFormData.description.value = data?.original_text || '';\n    setFormData(initFormData);\n    setImmData(initFormData);\n  }, [data]);\n\n  useEffect(() => {\n    const { displayName, slugName, description, editSummary } = formData;\n    const {\n      displayName: display_name,\n      slugName: slug_name,\n      description: original_text,\n    } = immData;\n\n    if (\n      display_name.value !== displayName.value ||\n      slug_name.value !== slugName.value ||\n      original_text.value !== description.value ||\n      editSummary.value\n    ) {\n      setContentChanged(true);\n    } else {\n      setContentChanged(false);\n    }\n  }, [\n    formData.displayName.value,\n    formData.slugName.value,\n    formData.description.value,\n    formData.editSummary.value,\n  ]);\n\n  const handleDescriptionChange = (value: string) =>\n    setFormData((prev) => ({\n      ...prev,\n      description: { value, isInvalid: false, errorMsg: '' },\n    }));\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { displayName, slugName } = formData;\n\n    if (!displayName.value) {\n      bol = false;\n      formData.displayName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.empty', {\n          keyPrefix: 'tag_modal',\n        }),\n      };\n    } else if (displayName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.display_name.msg.range', {\n          keyPrefix: 'tag_modal',\n        }),\n      };\n    } else {\n      formData.displayName = {\n        value: displayName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    if (!slugName.value) {\n      bol = false;\n      formData.slugName = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.empty', {\n          keyPrefix: 'tag_modal',\n        }),\n      };\n    } else if (slugName.value.length > TAG_SLUG_NAME_MAX_LENGTH) {\n      bol = false;\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: true,\n        errorMsg: t('form.fields.slug_name.msg.range', {\n          keyPrefix: 'tag_modal',\n        }),\n      };\n    } else {\n      formData.slugName = {\n        value: slugName.value,\n        isInvalid: false,\n        errorMsg: '',\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n    return bol;\n  };\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    setContentChanged(false);\n\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n\n    const params = {\n      display_name: formData.displayName.value,\n      slug_name: formData.slugName.value,\n      original_text: formData.description.value,\n      parsed_text: editorRef.current.getHtml(),\n      tag_id: data?.tag_id,\n      edit_summary: formData.editSummary.value,\n    };\n    modifyTag(params).then((res) => {\n      navigate(`/tags/${encodeURIComponent(formData.slugName.value)}/info`, {\n        replace: true,\n        state: { isReview: res.wait_for_review },\n      });\n    });\n  };\n\n  const handleSelectedRevision = (e) => {\n    const index = e.target.value;\n    const revision = revisions[index];\n    formData.description.value = revision.content.original_text;\n    formData.displayName.value = revision.content.display_name;\n    formData.slugName.value = revision.content.slug_name;\n    setImmData({ ...formData });\n    setFormData({ ...formData });\n  };\n\n  const handleDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      displayName: { ...formData.displayName, value: e.currentTarget.value },\n    });\n  };\n\n  const handleEditSummaryChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      editSummary: { ...formData.editSummary, value: e.currentTarget.value },\n    });\n  };\n\n  const handleSlugNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setFormData({\n      ...formData,\n      slugName: { ...formData.slugName, value: e.currentTarget.value },\n    });\n  };\n\n  const backPage = () => {\n    navigate(-1);\n  };\n  usePageTags({\n    title: t('edit_tag', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <div className=\"pt-4 mb-5\">\n      <h3 className=\"mb-4\">{t('title')}</h3>\n      <Row>\n        <Col className=\"page-main flex-auto\">\n          <Form noValidate onSubmit={handleSubmit}>\n            <Form.Group controlId=\"revision\" className=\"mb-3\">\n              <Form.Label>\n                {t('form.fields.revision.label', { keyPrefix: 'tag_modal' })}\n              </Form.Label>\n              <Form.Select onChange={handleSelectedRevision}>\n                {revisions.map(({ create_at, reason, user_info }, index) => {\n                  const date = dayjs(create_at * 1000)\n                    .tz()\n                    .format(t('long_date_with_time', { keyPrefix: 'dates' }));\n                  return (\n                    <option key={`${create_at}`} value={index}>\n                      {`${date} - ${user_info.display_name} - ${\n                        reason ||\n                        (index === revisions.length - 1\n                          ? t('default_first_reason')\n                          : t('default_reason'))\n                      }`}\n                    </option>\n                  );\n                })}\n              </Form.Select>\n            </Form.Group>\n            <Form.Group controlId=\"display_name\" className=\"mb-3\">\n              <Form.Label>\n                {t('form.fields.display_name.label', {\n                  keyPrefix: 'tag_modal',\n                })}\n              </Form.Label>\n              <Form.Control\n                value={formData.displayName.value}\n                isInvalid={formData.displayName.isInvalid}\n                disabled={role_id !== 2 && role_id !== 3}\n                onChange={handleDisplayNameChange}\n              />\n\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.displayName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"slug_name\" className=\"mb-3\">\n              <Form.Label>\n                {t('form.fields.slug_name.label', { keyPrefix: 'tag_modal' })}\n              </Form.Label>\n              <Form.Control\n                value={formData.slugName.value}\n                isInvalid={formData.slugName.isInvalid}\n                disabled={role_id !== 2 && role_id !== 3}\n                onChange={handleSlugNameChange}\n              />\n              <Form.Text as=\"div\">\n                {t('form.fields.slug_name.desc', { keyPrefix: 'tag_modal' })}\n              </Form.Text>\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.slugName.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <Form.Group controlId=\"description\" className=\"mt-4\">\n              <Form.Label>\n                {t('form.fields.desc.label', { keyPrefix: 'tag_modal' })}\n              </Form.Label>\n              <Editor\n                value={formData.description.value}\n                onChange={handleDescriptionChange}\n                className={classNames(\n                  'form-control p-0',\n                  focusType === 'description' && 'focus',\n                )}\n                onFocus={() => {\n                  setForceType('description');\n                }}\n                onBlur={() => {\n                  setForceType('');\n                }}\n                ref={editorRef}\n              />\n              <Form.Control\n                value={formData.description.value}\n                type=\"text\"\n                isInvalid={formData.description.isInvalid}\n                readOnly\n                hidden\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.description.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n            <Form.Group controlId=\"edit_summary\" className=\"my-3\">\n              <Form.Label>\n                {t('form.fields.edit_summary.label', {\n                  keyPrefix: 'tag_modal',\n                })}\n              </Form.Label>\n              <Form.Control\n                type=\"text\"\n                defaultValue={formData.editSummary.value}\n                isInvalid={formData.editSummary.isInvalid}\n                onChange={handleEditSummaryChange}\n                placeholder={t('form.fields.edit_summary.placeholder', {\n                  keyPrefix: 'tag_modal',\n                })}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.editSummary.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <div className=\"mt-3\">\n              <Button type=\"submit\">{t('btn_save_edits')}</Button>\n              <Button variant=\"link\" className=\"ms-2\" onClick={backPage}>\n                {t('btn_cancel')}\n              </Button>\n            </div>\n          </Form>\n        </Col>\n        <Col className=\"page-right-side mt-4 mt-xl-0\">\n          <Card>\n            <Card.Header>\n              {t('title', { keyPrefix: 'how_to_format' })}\n            </Card.Header>\n            <Card.Body\n              className=\"fmt small\"\n              dangerouslySetInnerHTML={{\n                __html: t('desc', { keyPrefix: 'how_to_format' }),\n              }}\n            />\n          </Card>\n        </Col>\n      </Row>\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Tags/Info/components/MergeTagModal/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.mergeTagModal {\n  .dropdown-item.active {\n    color: var(--bs-body-color);\n    background-color: var(--an-invite-answer-item-active);\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Tags/Info/components/MergeTagModal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect, useCallback, useRef } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Form, Dropdown, Modal, Button } from 'react-bootstrap';\n\nimport debounce from 'lodash/debounce';\n\nimport { TagInfo } from '@/common/interface';\nimport { queryTags } from '@/services';\n\nimport './index.scss';\n\nconst DEBOUNCE_DELAY = 400;\n\ninterface Props {\n  visible: boolean;\n  sourceTag: TagInfo;\n  onClose: () => void;\n  onConfirm: (sourceTagID: string, targetTagID: string) => void;\n}\n\ninterface SearchTagResp {\n  tag_id: string;\n  slug_name: string;\n  display_name: string;\n  recommend: boolean;\n  reserved: boolean;\n}\n\nconst MergeTagModal: FC<Props> = ({\n  visible,\n  sourceTag,\n  onClose,\n  onConfirm,\n}) => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'tag_info.merge',\n  });\n  const [targetTag, setTargetTag] = useState<SearchTagResp | null>(null);\n  const [searchValue, setSearchValue] = useState('');\n  const [tags, setTags] = useState<SearchTagResp[]>([]);\n  const [currentIndex, setCurrentIndex] = useState(0);\n  const [dropdownVisible, setDropdownVisible] = useState(false);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const searchTags = useCallback(\n    debounce((search) => {\n      if (!search) {\n        setTags([]);\n        return;\n      }\n      queryTags(search).then((res) => {\n        const filteredTags =\n          res.filter((tag) => tag.slug_name !== sourceTag.slug_name) || [];\n        setTags(filteredTags || []);\n        setDropdownVisible(true);\n      });\n    }, DEBOUNCE_DELAY),\n    [],\n  );\n\n  const handleConfirm = () => {\n    if (!targetTag) return;\n    onConfirm(sourceTag.tag_id, targetTag.tag_id);\n  };\n\n  const handleSelect = (tag: SearchTagResp) => {\n    setTargetTag(tag);\n    setDropdownVisible(false);\n    setSearchValue(tag.display_name);\n    inputRef.current?.blur();\n  };\n\n  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const searchStr = e.currentTarget.value.trim();\n    setSearchValue(searchStr);\n    searchTags(searchStr);\n  };\n\n  const handleBlur = () => {\n    setTimeout(() => {\n      if (!targetTag) {\n        setDropdownVisible(false);\n      }\n    }, 200);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (!tags.length) return;\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        setCurrentIndex((prev) => (prev < tags.length - 1 ? prev + 1 : prev));\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        setCurrentIndex((prev) => (prev > 0 ? prev - 1 : prev));\n        break;\n      case 'Enter':\n        e.preventDefault();\n        if (currentIndex >= 0 && currentIndex < tags.length) {\n          handleSelect(tags[currentIndex]);\n        }\n        break;\n      case 'Escape':\n        e.preventDefault();\n        inputRef.current?.blur();\n        setDropdownVisible(false);\n        break;\n      default:\n        break;\n    }\n  };\n\n  useEffect(() => {\n    if (visible) {\n      searchTags('');\n      setSearchValue('');\n      setTargetTag(null);\n      setCurrentIndex(0);\n      setDropdownVisible(false);\n    }\n  }, [visible]);\n\n  return (\n    <Modal show={visible} onCancel={onClose} className=\"mergeTagModal\">\n      <Modal.Header>\n        <Modal.Title>{t('title')}</Modal.Title>\n      </Modal.Header>\n      <Modal.Body>\n        <Form>\n          <Form.Group className=\"mb-3\">\n            <Form.Label>{t('source_tag_title')}</Form.Label>\n            <Form.Control value={sourceTag.display_name} disabled />\n            <Form.Text className=\"text-muted\">\n              {t('source_tag_description')}\n            </Form.Text>\n          </Form.Group>\n          <Form.Group>\n            <Form.Label>{t('target_tag_title')}</Form.Label>\n            <div className=\"position-relative\">\n              <Dropdown\n                show={Boolean(dropdownVisible && searchValue)}\n                onToggle={setDropdownVisible}>\n                <Form.Control\n                  ref={inputRef}\n                  type=\"text\"\n                  value={searchValue}\n                  onChange={handleSearch}\n                  onKeyDown={handleKeyDown}\n                  onBlur={handleBlur}\n                  autoComplete=\"off\"\n                />\n\n                <Dropdown.Menu className=\"w-100\">\n                  {tags.map((tag, index) => (\n                    <Dropdown.Item\n                      key={tag.slug_name}\n                      active={index === currentIndex}\n                      onClick={() => handleSelect(tag)}>\n                      {tag.display_name}\n                    </Dropdown.Item>\n                  ))}\n                  {!tags.length && searchValue && (\n                    <Dropdown.Item disabled>{t('no_results')}</Dropdown.Item>\n                  )}\n                </Dropdown.Menu>\n              </Dropdown>\n            </div>\n            <Form.Text className=\"text-muted\">\n              {t('target_tag_description')}\n            </Form.Text>\n          </Form.Group>\n        </Form>\n      </Modal.Body>\n      <Modal.Footer>\n        <Button variant=\"link\" onClick={onClose}>\n          {t('close', { keyPrefix: 'btns' })}\n        </Button>\n        <Button onClick={handleConfirm} disabled={!targetTag}>\n          {t('submit', { keyPrefix: 'btns' })}\n        </Button>\n      </Modal.Footer>\n    </Modal>\n  );\n};\n\nexport default MergeTagModal;\n"
  },
  {
    "path": "ui/src/pages/Tags/Info/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { Alert, Row, Col, Button, Card } from 'react-bootstrap';\nimport { useParams, useNavigate, Link, useLocation } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\n\nimport { usePageTags } from '@/hooks';\nimport { Tag, TagSelector, FormatTime, Modal, htmlRender } from '@/components';\nimport {\n  useTagInfo,\n  useQuerySynonymsTags,\n  saveSynonymsTags,\n  deleteTag,\n  editCheck,\n  unDeleteTag,\n  mergeTag,\n} from '@/services';\nimport { pathFactory } from '@/router/pathFactory';\nimport { loggedUserInfoStore, toastStore } from '@/stores';\n\nimport MergeTagModal from './components/MergeTagModal';\n\nconst TagIntroduction = () => {\n  const userInfo = loggedUserInfoStore((state) => state.user);\n  const location = useLocation();\n  const isLogged = Boolean(userInfo?.access_token);\n  const [isEdit, setEditState] = useState(false);\n  const { tagName } = useParams();\n  const { data: tagInfo, mutate: refreshTagInfo } = useTagInfo({\n    name: tagName,\n  });\n  const { t } = useTranslation('translation', { keyPrefix: 'tag_info' });\n  const navigate = useNavigate();\n  const { data: synonymsData, mutate } = useQuerySynonymsTags(\n    tagInfo?.tag_id,\n    tagInfo?.status,\n  );\n  const [showMergeModal, setShowMergeModal] = useState(false);\n\n  let pageTitle = '';\n  if (tagInfo) {\n    pageTitle = `'${tagInfo.display_name}' ${t('tag_wiki', {\n      keyPrefix: 'page_title',\n    })}`;\n  }\n  usePageTags({\n    title: pageTitle,\n  });\n  useEffect(() => {\n    if (location.state?.isReview) {\n      toastStore.getState().show({\n        msg: t('review', { keyPrefix: 'toast' }),\n        variant: 'warning',\n      });\n\n      // remove state isReview\n      const newLocation = { ...location };\n      delete newLocation.state;\n      window.history.replaceState(null, '', newLocation.pathname);\n    }\n  }, [location.state]);\n\n  useEffect(() => {\n    const fmt = document.querySelector('.content.fmt') as HTMLElement;\n    if (!fmt) {\n      return;\n    }\n    htmlRender(fmt, {\n      copySuccessText: t('copied', { keyPrefix: 'messages' }),\n      copyText: t('copy', { keyPrefix: 'messages' }),\n    });\n  }, [tagInfo?.parsed_text]);\n\n  if (!tagInfo) {\n    return null;\n  }\n  if (tagInfo.main_tag_slug_name) {\n    navigate(pathFactory.tagInfo(tagInfo.main_tag_slug_name), {\n      replace: true,\n    });\n    return null;\n  }\n\n  const handleEdit = () => {\n    setEditState(true);\n  };\n\n  const handleSave = () => {\n    saveSynonymsTags({\n      tag_id: tagInfo?.tag_id,\n      synonym_tag_list: synonymsData?.synonyms,\n    }).then(() => {\n      mutate();\n      setEditState(false);\n    });\n  };\n\n  const handleTagsChange = (value) => {\n    mutate(\n      { ...synonymsData, synonyms: [...value] },\n      {\n        revalidate: false,\n      },\n    );\n  };\n\n  const handleEditTag = () => {\n    editCheck(tagInfo?.tag_id).then(() => {\n      navigate(pathFactory.tagEdit(tagInfo?.tag_id));\n    });\n  };\n  const handleDeleteTag = () => {\n    if (synonymsData?.synonyms && synonymsData.synonyms.length > 0) {\n      Modal.confirm({\n        title: t('delete.title'),\n        content: t('delete.tip_with_synonyms'),\n        showConfirm: false,\n        cancelText: t('delete.close'),\n      });\n      return;\n    }\n    if (tagInfo.question_count > 0) {\n      Modal.confirm({\n        title: t('delete.title'),\n        content: t('delete.tip_with_posts'),\n        showConfirm: false,\n        cancelText: t('delete.close'),\n      });\n      return;\n    }\n\n    Modal.confirm({\n      title: t('delete.title'),\n      content: t('delete.tip'),\n      confirmText: t('delete', { keyPrefix: 'btns' }),\n      confirmBtnVariant: 'danger',\n      onConfirm: () => {\n        deleteTag(tagInfo.tag_id).then(() => {\n          navigate('/tags', { replace: true });\n        });\n      },\n    });\n  };\n  const handleMergeTag = () => {\n    setShowMergeModal(true);\n  };\n\n  const handleMergeConfirm = (sourceTagID: string, targetTagID: string) => {\n    mergeTag({ source_tag_id: sourceTagID, target_tag_id: targetTagID }).then(\n      () => {\n        setShowMergeModal(false);\n        navigate('/tags', { replace: true });\n      },\n    );\n  };\n\n  const onAction = (params) => {\n    if (params.action === 'edit') {\n      handleEditTag();\n    }\n    if (params.action === 'delete') {\n      handleDeleteTag();\n    }\n    if (params.action === 'merge') {\n      handleMergeTag();\n    }\n    if (params.action === 'undelete') {\n      Modal.confirm({\n        title: t('undelete_title', { keyPrefix: 'delete' }),\n        content: t('undelete_desc', { keyPrefix: 'delete' }),\n        cancelBtnVariant: 'link',\n        confirmBtnVariant: 'danger',\n        confirmText: t('undelete', { keyPrefix: 'btns' }),\n        onConfirm: () => {\n          unDeleteTag(tagInfo.tag_id).then(() => {\n            // undo\n            refreshTagInfo();\n          });\n        },\n      });\n    }\n  };\n\n  return (\n    <>\n      <Row className=\"pt-4 mb-5\">\n        <Col className=\"page-main flex-auto\">\n          {tagInfo?.status === 'deleted' && (\n            <Alert variant=\"danger\" className=\"mb-4\">\n              {t('post_deleted', { keyPrefix: 'messages' })}\n            </Alert>\n          )}\n          <h3 className=\"mb-3\">\n            <Link\n              to={pathFactory.tagLanding(tagInfo.slug_name)}\n              replace\n              className=\"link-dark\">\n              {tagInfo.display_name}\n            </Link>\n          </h3>\n\n          <div className=\"text-secondary mb-4 small\">\n            <FormatTime preFix={t('created_at')} time={tagInfo.created_at} />\n            <FormatTime\n              preFix={t('edited_at')}\n              className=\"ms-3\"\n              time={tagInfo.updated_at}\n            />\n          </div>\n\n          <div\n            className=\"content text-break fmt\"\n            dangerouslySetInnerHTML={{ __html: tagInfo?.parsed_text }}\n          />\n          <div className=\"mt-4\">\n            {tagInfo?.member_actions.map((action, index) => {\n              return (\n                <Button\n                  key={action.name}\n                  variant=\"link\"\n                  size=\"sm\"\n                  className={classNames(\n                    'link-secondary btn-no-border p-0',\n                    index > 0 && 'ms-3',\n                  )}\n                  onClick={() => onAction(action)}>\n                  {action.name}\n                </Button>\n              );\n            })}\n            {isLogged && (\n              <Link\n                to={`/tags/${tagInfo?.tag_id}/timeline`}\n                className={classNames(\n                  'link-secondary btn-no-border p-0 small',\n                  tagInfo?.member_actions?.length > 0 && 'ms-3',\n                )}>\n                {t('history')}\n              </Link>\n            )}\n          </div>\n        </Col>\n        <Col className=\"page-right-side mt-4 mt-xl-0\">\n          <Card>\n            <Card.Header className=\"d-flex justify-content-between\">\n              <span>{t('synonyms.title')}</span>\n              {isEdit ? (\n                <Button\n                  variant=\"link\"\n                  className=\"p-0 btn-no-border\"\n                  onClick={handleSave}>\n                  {t('synonyms.btn_save')}\n                </Button>\n              ) : synonymsData?.member_actions?.find(\n                  (v) => v.action === 'edit',\n                ) ? (\n                <Button\n                  variant=\"link\"\n                  className=\"p-0 btn-no-border\"\n                  onClick={handleEdit}>\n                  {t('synonyms.btn_edit')}\n                </Button>\n              ) : null}\n            </Card.Header>\n            <Card.Body>\n              {isEdit && (\n                <>\n                  <div className=\"mb-3\">\n                    {t('synonyms.text')}{' '}\n                    <Tag\n                      data={{\n                        slug_name: tagName || '',\n                        main_tag_slug_name: '',\n                        display_name:\n                          tagInfo?.display_name || tagInfo?.slug_name || '',\n                        recommend: false,\n                        reserved: false,\n                      }}\n                    />\n                  </div>\n                  <TagSelector\n                    value={synonymsData?.synonyms}\n                    onChange={handleTagsChange}\n                    hiddenDescription\n                  />\n                </>\n              )}\n              {!isEdit &&\n                (synonymsData?.synonyms && synonymsData.synonyms.length > 0 ? (\n                  <div className=\"m-n1\">\n                    {synonymsData.synonyms.map((item) => {\n                      return (\n                        <Tag key={item.tag_id} className=\"m-1\" data={item} />\n                      );\n                    })}\n                  </div>\n                ) : (\n                  <>\n                    <div className=\"text-muted mb-3\">{t('synonyms.empty')}</div>\n                    {synonymsData?.member_actions?.find(\n                      (v) => v.action === 'edit',\n                    ) && (\n                      <Button\n                        variant=\"outline-primary\"\n                        size=\"sm\"\n                        onClick={handleEdit}>\n                        {t('synonyms.btn_add')}\n                      </Button>\n                    )}\n                  </>\n                ))}\n            </Card.Body>\n          </Card>\n        </Col>\n      </Row>\n      <MergeTagModal\n        visible={showMergeModal}\n        sourceTag={tagInfo}\n        onClose={() => setShowMergeModal(false)}\n        onConfirm={handleMergeConfirm}\n      />\n    </>\n  );\n};\n\nexport default TagIntroduction;\n"
  },
  {
    "path": "ui/src/pages/Tags/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState } from 'react';\nimport { Row, Col, Card, Button, Form, Stack } from 'react-bootstrap';\nimport { useSearchParams, Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags, useSkeletonControl } from '@/hooks';\nimport { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';\nimport { formatCount, escapeRemove } from '@/utils';\nimport { tryNormalLogged } from '@/utils/guard';\nimport { useQueryTags, following } from '@/services';\nimport { loggedUserInfoStore } from '@/stores';\n\nconst sortBtns = ['popular', 'name', 'newest'];\n\nconst Tags = () => {\n  const [urlSearch, setUrlSearch] = useSearchParams();\n  const { t } = useTranslation('translation', { keyPrefix: 'tags' });\n  const [searchTag, setSearchTag] = useState('');\n  const { role_id } = loggedUserInfoStore((_) => _.user);\n\n  const page = Number(urlSearch.get('page')) || 1;\n  const sort = urlSearch.get('sort') || sortBtns[0];\n\n  const pageSize = 20;\n  const {\n    data: tags,\n    mutate,\n    isLoading,\n  } = useQueryTags({\n    page,\n    page_size: pageSize,\n    ...(searchTag ? { slug_name: searchTag } : {}),\n    ...(sort ? { query_cond: sort } : {}),\n  });\n\n  const { isSkeletonShow } = useSkeletonControl(isLoading);\n\n  const handleChange = (e) => {\n    setSearchTag(e.target.value);\n    setUrlSearch((prev) => {\n      prev.set('page', '1');\n      return prev;\n    });\n  };\n\n  const handleFollow = (tag) => {\n    if (!tryNormalLogged(true)) {\n      return;\n    }\n    following({\n      object_id: tag.tag_id,\n      is_cancel: tag.is_follower,\n    }).then(() => {\n      mutate();\n    });\n  };\n\n  usePageTags({\n    title: t('tags', { keyPrefix: 'page_title' }),\n  });\n\n  return (\n    <Row className=\"py-4 mb-4\">\n      <Col xxl={12}>\n        <h3 className=\"mb-4\">{t('title')}</h3>\n        <div className=\"d-block d-sm-flex justify-content-between align-items-center flex-wrap\">\n          <Stack direction=\"horizontal\" gap={3} className=\"mb-3 mb-sm-0\">\n            <Form>\n              <Form.Group controlId=\"formBasicEmail\">\n                <Form.Control\n                  value={searchTag}\n                  placeholder={t('search_placeholder')}\n                  type=\"search\"\n                  onChange={handleChange}\n                  size=\"sm\"\n                />\n              </Form.Group>\n            </Form>\n            {role_id === 2 || role_id === 3 ? (\n              <Link\n                className=\"btn btn-outline-primary btn-sm\"\n                to=\"/tags/create\">\n                {t('title', { keyPrefix: 'tag_modal' })}\n              </Link>\n            ) : null}\n          </Stack>\n          <QueryGroup\n            data={sortBtns}\n            currentSort={sort || 'popular'}\n            sortKey=\"sort\"\n            i18nKeyPrefix=\"tags.sort_buttons\"\n          />\n        </div>\n      </Col>\n\n      <Col className=\"mt-4\" xxl={12}>\n        <Row>\n          {isSkeletonShow ? (\n            <TagsLoader />\n          ) : (\n            tags?.list?.map((tag) => (\n              <Col\n                key={tag.slug_name}\n                xl={3}\n                lg={4}\n                md={4}\n                sm={6}\n                xs={12}\n                className=\"mb-4\">\n                <Card className=\"h-100\">\n                  <Card.Body className=\"d-flex flex-column align-items-start\">\n                    <Tag className=\"mb-3\" data={tag} />\n\n                    <div className=\"small flex-fill text-break text-wrap text-truncate-3 reset-p mb-3\">\n                      {escapeRemove(tag.excerpt)}\n                    </div>\n                    <div className=\"d-flex align-items-center\">\n                      <Button\n                        className={`me-2 ${tag.is_follower ? 'active' : ''}`}\n                        variant=\"outline-primary\"\n                        size=\"sm\"\n                        onClick={() => handleFollow(tag)}>\n                        {tag.is_follower\n                          ? t('button_following')\n                          : t('button_follow')}\n                      </Button>\n                      <span className=\"text-secondary small text-nowrap\">\n                        {formatCount(tag.question_count)} {t('tag_label')}\n                      </span>\n                    </div>\n                  </Card.Body>\n                </Card>\n              </Col>\n            ))\n          )}\n        </Row>\n        <div className=\"d-flex justify-content-center\">\n          <Pagination\n            currentPage={page}\n            totalSize={tags?.count || 0}\n            pageSize={pageSize}\n          />\n        </div>\n      </Col>\n    </Row>\n  );\n};\n\nexport default Tags;\n"
  },
  {
    "path": "ui/src/pages/Timeline/components/Item/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState } from 'react';\nimport { Button, Row, Col } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';\nimport { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';\nimport * as Type from '@/common/interface';\nimport { getTimelineDetail } from '@/services';\n\ninterface Props {\n  data: Type.TimelineItem;\n  objectInfo: Type.TimelineObject;\n  isAdmin: boolean;\n  revisionList: Type.TimelineItem[];\n}\nconst Index: FC<Props> = ({ data, isAdmin, objectInfo, revisionList }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'timeline' });\n  const [isOpen, setIsOpen] = useState(false);\n  const [detailData, setDetailData] = useState({\n    new_revision: {},\n    old_revision: {},\n  });\n\n  const handleItemClick = async (id) => {\n    if (!isOpen) {\n      const revisionItem = revisionList?.find((v) => v.revision_id === id);\n      let oldId;\n      if (revisionList?.length > 0 && revisionItem) {\n        const idIndex = revisionList.indexOf(revisionItem) || 0;\n        if (idIndex === revisionList.length - 1) {\n          oldId = 0;\n        } else {\n          oldId = revisionList[idIndex + 1].revision_id;\n        }\n      }\n      const res = await getTimelineDetail({\n        new_revision_id: id,\n        old_revision_id: oldId,\n      });\n      setDetailData(res);\n    }\n    setIsOpen(!isOpen);\n  };\n\n  return (\n    <>\n      <tr>\n        <td>\n          <FormatTime time={data.created_at} />\n          <br />\n          {data.cancelled_at > 0 && <FormatTime time={data.cancelled_at} />}\n        </td>\n        <td className=\"text-nowrap\">\n          {(data.activity_type === 'rollback' ||\n            data.activity_type === 'edited' ||\n            data.activity_type === 'asked' ||\n            data.activity_type === 'created' ||\n            (objectInfo.object_type === 'answer' &&\n              data.activity_type === 'answered')) && (\n            <Button\n              onClick={() => handleItemClick(data.revision_id)}\n              variant=\"link\"\n              className=\"text-body p-0 btn-no-border\">\n              <Icon\n                name=\"caret-right-fill\"\n                className={`me-1 ${isOpen ? 'rotate-90-deg' : 'rotate-0-deg'}`}\n              />\n              {t(data.activity_type)}\n            </Button>\n          )}\n          {data.activity_type === 'accept' && (\n            <Link\n              to={`/questions/${objectInfo.question_id}/${data?.object_id}`}>\n              {t(data.activity_type)}\n            </Link>\n          )}\n\n          {objectInfo.object_type === 'question' &&\n            data.activity_type === 'answered' && (\n              <Link\n                to={`/questions/${objectInfo.question_id}/${data.object_id}`}>\n                {t(data.activity_type)}\n              </Link>\n            )}\n\n          {data.activity_type === 'commented' && (\n            <Link\n              to={\n                objectInfo.object_type === 'answer'\n                  ? `/questions/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`\n                  : `/questions/${objectInfo.question_id}?commentId=${data.object_id}`\n              }>\n              {t(data.activity_type)}\n            </Link>\n          )}\n\n          {TIMELINE_NORMAL_ACTIVITY_TYPE.includes(data.activity_type) && (\n            <div>{t(data.activity_type)}</div>\n          )}\n\n          {data.cancelled && (\n            <div className=\"text-danger\">{t('cancelled')}</div>\n          )}\n        </td>\n        <td>\n          {data.activity_type === 'downvote' && !isAdmin ? (\n            <div>{t('n_or_a')}</div>\n          ) : (\n            <BaseUserCard\n              className=\"fs-normal\"\n              data={data?.user_info}\n              showAvatar={false}\n              showReputation={false}\n            />\n          )}\n        </td>\n        <td>\n          <div dangerouslySetInnerHTML={{ __html: data.comment }} />\n        </td>\n      </tr>\n      <tr className={isOpen ? '' : 'd-none'}>\n        <td colSpan={5} className=\"p-0 py-5\">\n          <Row className=\"justify-content-center\">\n            <Col xxl={8}>\n              <DiffContent\n                objectType={objectInfo.object_type}\n                newData={detailData?.new_revision}\n                oldData={detailData?.old_revision}\n              />\n            </Col>\n          </Row>\n        </td>\n      </tr>\n    </>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Timeline/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.min-w-15 {\n  min-width: 15rem;\n}\n\n.max-w-30 {\n  max-width: 30rem;\n}\n\n@media screen and (max-width: 768px) {\n  .max-w-30 {\n    max-width: 15rem;\n  }\n}\n\n.table tr th {\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "ui/src/pages/Timeline/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, useState, useEffect } from 'react';\nimport { Form, Table } from 'react-bootstrap';\nimport { Link, useParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { pathFactory } from '@/router/pathFactory';\nimport { loggedUserInfoStore } from '@/stores';\nimport { getTimelineData } from '@/services';\nimport { Empty } from '@/components';\nimport * as Type from '@/common/interface';\n\nimport HistoryItem from './components/Item';\n\nimport './index.scss';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'timeline' });\n  const { qid = '', aid = '', tid = '' } = useParams();\n  const { role_id } = loggedUserInfoStore((state) => state.user);\n  const [showVotes, setShowVotes] = useState(false);\n  const [isLoading, setLoading] = useState(false);\n  const [timelineData, setTimelineData] = useState<Type.TimelineRes>();\n\n  const getPageData = (bol: boolean) => {\n    setLoading(true);\n    getTimelineData({\n      object_id: tid || aid || qid,\n      show_vote: bol,\n    })\n      .then((res) => {\n        setTimelineData(res);\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  };\n\n  const handleSwitch = (bol: boolean) => {\n    setShowVotes(bol);\n    getPageData(bol);\n  };\n\n  useEffect(() => {\n    getPageData(false);\n  }, []);\n\n  let linkUrl = '';\n  let pageTitle = '';\n  if (timelineData?.object_info.object_type === 'question') {\n    linkUrl = pathFactory.questionLanding(\n      timelineData?.object_info.question_id,\n      timelineData?.object_info.url_title,\n    );\n    pageTitle = `${t('title_for_question')} ${timelineData?.object_info.title}`;\n  }\n\n  if (timelineData?.object_info.object_type === 'answer') {\n    linkUrl = pathFactory.answerLanding({\n      questionId: timelineData?.object_info.question_id,\n      slugTitle: timelineData?.object_info.url_title,\n      answerId: timelineData?.object_info.answer_id,\n    });\n    pageTitle = `${t('title_for_answer', {\n      title: timelineData?.object_info.title,\n      author: timelineData?.object_info.display_name,\n    })}`;\n  }\n\n  if (timelineData?.object_info.object_type === 'tag') {\n    linkUrl = `/tags/${\n      timelineData?.object_info.main_tag_slug_name\n        ? encodeURIComponent(timelineData?.object_info.main_tag_slug_name)\n        : encodeURIComponent(timelineData?.object_info.title)\n    }`;\n    pageTitle = `${t('title_for_tag')} '${timelineData?.object_info.title}'`;\n  }\n\n  const revisionList =\n    timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];\n  usePageTags({\n    title: pageTitle,\n  });\n  return (\n    <div className=\"py-4 mb-5\">\n      <h5 className=\"mb-4\">\n        {timelineData?.object_info.object_type === 'tag'\n          ? t('tag_title')\n          : t('title')}{' '}\n        <Link to={linkUrl}>{timelineData?.object_info?.title}</Link>\n      </h5>\n      {timelineData?.object_info.object_type !== 'tag' && (\n        <Form.Check\n          className=\"mb-4\"\n          type=\"switch\"\n          id=\"custom-switch\"\n          label={t('show_votes')}\n          checked={showVotes}\n          onChange={(e) => handleSwitch(e.target.checked)}\n        />\n      )}\n      <Table hover responsive=\"md\">\n        <thead>\n          <tr>\n            <th style={{ width: '20%' }}>{t('datetime')}</th>\n            <th style={{ width: '15%' }}>{t('type')}</th>\n            <th style={{ width: '19%' }}>{t('by')}</th>\n            <th className=\"min-w-15\">{t('comment')}</th>\n          </tr>\n        </thead>\n        <tbody>\n          {timelineData?.timeline?.map((item) => {\n            return (\n              <HistoryItem\n                data={item}\n                objectInfo={timelineData?.object_info}\n                key={item.activity_id}\n                isAdmin={role_id === 2}\n                revisionList={revisionList}\n              />\n            );\n          })}\n        </tbody>\n      </Table>\n      {!isLoading && Number(timelineData?.timeline?.length) <= 0 && (\n        <Empty>{t('no_data')}</Empty>\n      )}\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/UserCenter/Auth/components/WeCom/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { memo, FC, useState, useEffect } from 'react';\nimport { Card } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useNavigate } from 'react-router-dom';\n\nimport QrCode from 'qrcode';\n\nimport { userCenterStore } from '@/stores';\nimport { guard, getUaType, floppyNavigation } from '@/utils';\nimport { USER_AGENT_NAMES } from '@/common/constants';\n\nimport { getLoginConf, checkLoginResult } from './service';\n\nlet checkTimer: NodeJS.Timeout;\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'user_center' });\n  const navigate = useNavigate();\n  const ucAgent = userCenterStore().agent;\n  const agentName = ucAgent?.agent_info?.name || '';\n  const [qrcodeDataUrl, setQrCodeDataUrl] = useState('');\n  const handleLoginResult = (key: string) => {\n    if (!key) {\n      return;\n    }\n    checkLoginResult(key).then((res) => {\n      if (res.is_login) {\n        guard.handleLoginWithToken(res.token, navigate);\n        return;\n      }\n      clearTimeout(checkTimer);\n      checkTimer = setTimeout(() => {\n        handleLoginResult(key);\n      }, 2000);\n    });\n  };\n  const handleQrCode = (targetUrl: string) => {\n    if (!targetUrl) {\n      return;\n    }\n    QrCode.toDataURL(targetUrl, { width: 240, margin: 0 }, (err, url) => {\n      if (err) {\n        return;\n      }\n      setQrCodeDataUrl(url);\n    });\n  };\n\n  useEffect(() => {\n    if (!agentName) {\n      return;\n    }\n    getLoginConf().then((res) => {\n      if (getUaType() === USER_AGENT_NAMES.WeCom) {\n        floppyNavigation.navigate(res?.redirect_url, {\n          handler: 'replace',\n        });\n      } else {\n        handleQrCode(res?.redirect_url);\n        handleLoginResult(res?.key);\n      }\n    });\n  }, [agentName]);\n  useEffect(() => {\n    return () => {\n      clearTimeout(checkTimer);\n    };\n  }, []);\n\n  if (getUaType() !== USER_AGENT_NAMES.WeCom) {\n    return (\n      <Card className=\"text-center\">\n        <Card.Body>\n          <Card.Title as=\"h3\" className=\"mb-3\">\n            {ucAgent?.agent_info?.display_name} {t('login')}\n          </Card.Title>\n          {qrcodeDataUrl ? (\n            <>\n              <img\n                className=\"w-100\"\n                style={{ maxWidth: '240px' }}\n                src={qrcodeDataUrl}\n                alt={agentName}\n              />\n              <div className=\"text-secondary mt-3\">\n                {t('qrcode_login_tip', {\n                  agentName: ucAgent?.agent_info?.display_name,\n                })}\n              </div>\n            </>\n          ) : null}\n        </Card.Body>\n      </Card>\n    );\n  }\n  return null;\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/UserCenter/Auth/components/WeCom/service.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\n\ntype loginConf = {\n  key: string;\n  redirect_url: string;\n};\n\ntype loginResult = {\n  is_login: boolean;\n  token: string;\n};\n\nexport const getLoginConf = () => {\n  const apiUrl = `/answer/api/v1/wecom/login/url`;\n  return request.get<loginConf>(apiUrl);\n};\n\nexport const checkLoginResult = (key: loginConf['key']) => {\n  const apiUrl = `/answer/api/v1/wecom/login/check?key=${key}`;\n  return request.get<loginResult>(apiUrl);\n};\n"
  },
  {
    "path": "ui/src/pages/UserCenter/Auth/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { Container, Col } from 'react-bootstrap';\nimport { useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { userCenterStore } from '@/stores';\nimport { USER_AGENT_NAMES } from '@/common/constants';\nimport { usePageTags } from '@/hooks';\n\nimport WeComAuth from './components/WeCom';\n\nconst Index = () => {\n  const { t } = useTranslation('translation');\n  const [searchParam] = useSearchParams();\n  const { agent: ucAgent } = userCenterStore();\n  let agentName = ucAgent?.agent_info?.name || '';\n  if (searchParam.get('agent_name')) {\n    agentName = searchParam.get('agent_name') || '';\n  }\n  usePageTags({\n    title: t('login', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container style={{ paddingTop: '5rem', paddingBottom: '5rem' }}>\n      <Col md={4} className=\"mx-auto\">\n        {USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (\n          <WeComAuth />\n        ) : null}\n      </Col>\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/UserCenter/AuthFailed/components/WeCom.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Card, Col, Carousel } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { userCenterStore } from '@/stores';\n\nconst data = [\n  {\n    id: 1,\n    url: require('@/assets/images/carousel-wecom-1.jpg'),\n  },\n  {\n    id: 2,\n    url: require('@/assets/images/carousel-wecom-2.jpg'),\n  },\n  {\n    id: 3,\n    url: require('@/assets/images/carousel-wecom-3.jpg'),\n  },\n  {\n    id: 4,\n    url: require('@/assets/images/carousel-wecom-4.jpg'),\n  },\n  {\n    id: 5,\n    url: require('@/assets/images/carousel-wecom-5.jpg'),\n  },\n];\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'user_center' });\n  const ucAgent = userCenterStore().agent;\n  return (\n    <Col lg={4} className=\"mx-auto mt-3 py-5\">\n      <Card>\n        <Card.Body>\n          <h3 className=\"text-center pt-3 mb-3\">\n            {ucAgent?.agent_info?.display_name} {t('login')}\n          </h3>\n          <p className=\"text-danger text-center\">\n            {t('login_failed_email_tip')}\n          </p>\n\n          <Carousel controls={false}>\n            {data.map((item) => (\n              <Carousel.Item key={item.id}>\n                <img\n                  className=\"d-block w-100\"\n                  src={item.url}\n                  alt=\"First slide\"\n                />\n              </Carousel.Item>\n            ))}\n          </Carousel>\n        </Card.Body>\n      </Card>\n    </Col>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/UserCenter/AuthFailed/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { Container } from 'react-bootstrap';\nimport { useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { userCenterStore } from '@/stores';\nimport { USER_AGENT_NAMES } from '@/common/constants';\nimport { usePageTags } from '@/hooks';\n\nimport WeCom from './components/WeCom';\n\nconst Index = () => {\n  const { t } = useTranslation('translation');\n  const [searchParam] = useSearchParams();\n  const { agent: ucAgent } = userCenterStore();\n  let agentName = ucAgent?.agent_info?.name || '';\n  if (searchParam.get('agent_name')) {\n    agentName = searchParam.get('agent_name') || '';\n  }\n  usePageTags({\n    title: t('login', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container>\n      {USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (\n        <WeCom />\n      ) : null}\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/AccountForgot/components/sendEmail.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport type { PasswordResetReq, FormDataType } from '@/common/interface';\nimport { resetPassword } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\n\ninterface IProps {\n  // eslint-disable-next-line react/no-unused-prop-types\n  visible?: boolean;\n  callback: (param: number, email: string) => void;\n}\n\nconst Index: FC<IProps> = ({ callback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });\n  const [formData, setFormData] = useState<FormDataType>({\n    e_mail: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const emailCaptcha = useCaptchaPlugin('email');\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n\n    if (!formData.e_mail.value) {\n      bol = false;\n      formData.e_mail = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('email.msg.empty'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const ele = document.getElementById('email');\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const sendEmail = (e?: any) => {\n    if (e) {\n      e.preventDefault();\n    }\n    const params: PasswordResetReq = {\n      e_mail: formData.e_mail.value,\n    };\n\n    const captcha = emailCaptcha?.getCaptcha();\n    if (captcha?.verify) {\n      params.captcha_code = captcha.captcha_code;\n      params.captcha_id = captcha.captcha_id;\n    }\n\n    resetPassword(params)\n      .then(async () => {\n        await emailCaptcha?.close();\n        callback?.(2, formData.e_mail.value);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          emailCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = async (event: any) => {\n    event.preventDefault();\n    event.stopPropagation();\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (!emailCaptcha) {\n      sendEmail();\n      return;\n    }\n\n    emailCaptcha.check(() => {\n      sendEmail();\n    });\n  };\n\n  return (\n    <Form noValidate onSubmit={handleSubmit} autoComplete=\"off\">\n      <Form.Group controlId=\"email\" className=\"mb-3\">\n        <Form.Label>{t('email.label')}</Form.Label>\n        <Form.Control\n          required\n          type=\"email\"\n          value={formData.e_mail.value}\n          isInvalid={formData.e_mail.isInvalid}\n          onChange={(e) => {\n            handleChange({\n              e_mail: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Control.Feedback type=\"invalid\">\n          {formData.e_mail.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <div className=\"d-grid mb-3\">\n        <Button variant=\"primary\" type=\"submit\">\n          {t('btn_name')}\n        </Button>\n      </div>\n    </Form>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/AccountForgot/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState } from 'react';\nimport { Container, Col } from 'react-bootstrap';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\n\nimport SendEmail from './components/sendEmail';\n\nconst Index: React.FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });\n  const [step, setStep] = useState(1);\n  const [email, setEmail] = useState('');\n\n  const callback = (param: number, mail: string) => {\n    setStep(param);\n    setEmail(mail);\n  };\n  usePageTags({\n    title: t('account_recovery', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>\n      <h3 className=\"text-center mb-5\">{t('page_title')}</h3>\n      {step === 1 && (\n        <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n          <SendEmail visible={step === 1} callback={callback} />\n        </Col>\n      )}\n      {step === 2 && (\n        <Col className=\"mx-auto px-4\" md={6}>\n          <div className=\"text-center\">\n            <p>\n              <Trans\n                i18nKey=\"account_forgot.send_success\"\n                values={{ mail: email }}\n                components={{ bold: <strong /> }}\n              />\n            </p>\n          </div>\n        </Col>\n      )}\n    </Container>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/ActivationResult/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Container, Row, Col } from 'react-bootstrap';\nimport { Link, useLocation } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { WelcomeTitle } from '@/components';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'account_result' });\n  const location = useLocation();\n  usePageTags({\n    title: t('account_activation', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container className=\"pt-4 mt-2 mb-5\">\n      <Row className=\"justify-content-center\">\n        <Col lg={6}>\n          {location.pathname?.includes('success') && (\n            <>\n              <WelcomeTitle className=\"mt-3 mb-5\" />\n              <p className=\"text-center\">{t('success')}</p>\n              <div className=\"text-center\">\n                <Link to=\"/\">{t('link')}</Link>\n              </div>\n            </>\n          )}\n\n          {location.pathname?.includes('failed') && (\n            <div className=\"d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center\">\n              <div\n                className=\"mb-4 text-secondary\"\n                style={{ fontSize: '120px', lineHeight: 1.2 }}>\n                (=‘x‘=)\n              </div>\n\n              <h4 className=\"text-center\">{t('oops')}</h4>\n              <p className=\"text-center mb-3 fs-5\">{t('invalid')}</p>\n              <div className=\"text-center\">\n                <Link to=\"/\" className=\"btn btn-link\">\n                  {t('back_home', { keyPrefix: 'page_error' })}\n                </Link>\n              </div>\n            </div>\n          )}\n        </Col>\n      </Row>\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/ActiveEmail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { loggedUserInfoStore } from '@/stores';\nimport { activateAccount } from '@/services';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_title' });\n  const [searchParams] = useSearchParams();\n  const updateUser = loggedUserInfoStore((state) => state.update);\n  const navigate = useNavigate();\n  useEffect(() => {\n    const code = searchParams.get('code');\n\n    if (code) {\n      activateAccount(encodeURIComponent(code)).then((res) => {\n        updateUser(res);\n        setTimeout(() => {\n          navigate('/users/account-activation/success', { replace: true });\n        }, 0);\n      });\n    } else {\n      navigate('/', { replace: true });\n    }\n  }, []);\n  usePageTags({\n    title: t('account_activation'),\n  });\n  return null;\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/AuthCallback/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { guard } from '@/utils';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_title' });\n  const [searchParams] = useSearchParams();\n  const navigate = useNavigate();\n  useEffect(() => {\n    const token = searchParams.get('access_token');\n    guard.handleLoginWithToken(token, navigate);\n  }, []);\n  usePageTags({\n    title: t('oauth_callback'),\n  });\n  return null;\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useNavigate } from 'react-router-dom';\n\nimport type { PasswordResetReq, FormDataType } from '@/common/interface';\nimport { loggedUserInfoStore } from '@/stores';\nimport { changeEmail } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'change_email' });\n  const [formData, setFormData] = useState<FormDataType>({\n    e_mail: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const navigate = useNavigate();\n  const { user: userInfo, update: updateUser } = loggedUserInfoStore();\n\n  const emailCaptcha = useCaptchaPlugin('email');\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n\n    if (!formData.e_mail.value) {\n      bol = false;\n      formData.e_mail = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('email.msg.empty'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    return bol;\n  };\n\n  const sendEmail = (e?: any) => {\n    if (e) {\n      e.preventDefault();\n    }\n    const params: PasswordResetReq = {\n      e_mail: formData.e_mail.value,\n    };\n    const imgCode = emailCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      params.captcha_code = imgCode.captcha_code;\n      params.captcha_id = imgCode.captcha_id;\n    }\n\n    changeEmail(params)\n      .then(async () => {\n        await emailCaptcha?.close();\n        userInfo.e_mail = formData.e_mail.value;\n        updateUser(userInfo);\n        navigate('/users/login', { replace: true });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          emailCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = async (event: any) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    if (!emailCaptcha) {\n      sendEmail();\n      return;\n    }\n    emailCaptcha.check(() => {\n      sendEmail();\n    });\n  };\n\n  const goBack = () => {\n    navigate('/users/login?status=inactive', { replace: true });\n  };\n\n  return (\n    <Form noValidate onSubmit={handleSubmit} autoComplete=\"off\">\n      <Form.Group controlId=\"email\" className=\"mb-3\">\n        <Form.Label>{t('email.label')}</Form.Label>\n        <Form.Control\n          required\n          type=\"email\"\n          value={formData.e_mail.value}\n          isInvalid={formData.e_mail.isInvalid}\n          onChange={(e) => {\n            handleChange({\n              e_mail: {\n                value: e.target.value,\n                isInvalid: false,\n                errorMsg: '',\n              },\n            });\n          }}\n        />\n        <Form.Control.Feedback type=\"invalid\">\n          {formData.e_mail.errorMsg}\n        </Form.Control.Feedback>\n      </Form.Group>\n\n      <div className=\"d-grid mb-3\">\n        <Button variant=\"primary\" type=\"submit\">\n          {t('btn_update')}\n        </Button>\n        <Button variant=\"link\" className=\"mt-2 d-block\" onClick={goBack}>\n          {t('btn_cancel')}\n        </Button>\n      </div>\n    </Form>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/ChangeEmail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Container, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { WelcomeTitle } from '@/components';\n\nimport SendEmail from './components/sendEmail';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'change_email' });\n  usePageTags({\n    title: t('change_email', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>\n      <WelcomeTitle />\n      <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n        <SendEmail />\n      </Col>\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/ConfirmNewEmail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect, useState } from 'react';\nimport { Container, Row, Col } from 'react-bootstrap';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { loggedUserInfoStore } from '@/stores';\nimport { changeEmailVerify } from '@/services';\nimport { WelcomeTitle } from '@/components';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'account_result' });\n  const [searchParams] = useSearchParams();\n  const [step, setStep] = useState('loading');\n\n  const updateUser = loggedUserInfoStore((state) => state.update);\n\n  useEffect(() => {\n    const code = searchParams.get('code');\n    if (code) {\n      // do\n      changeEmailVerify({ code })\n        .then((res) => {\n          setStep('success');\n          if (res?.access_token) {\n            // update user info\n            updateUser(res);\n          }\n        })\n        .catch(() => {\n          setStep('invalid');\n        });\n    }\n  }, []);\n  usePageTags({\n    title: t('confirm_email', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container className=\"pt-4 mt-2 mb-5\">\n      <Row className=\"justify-content-center\">\n        <Col lg={6}>\n          <WelcomeTitle className=\"mt-3 mb-5\" />\n          {step === 'success' && (\n            <>\n              <p className=\"text-center\">{t('confirm_new_email')}</p>\n              <div className=\"text-center\">\n                <Link to=\"/\">{t('link')}</Link>\n              </div>\n            </>\n          )}\n\n          {step === 'invalid' && (\n            <p className=\"text-center\">{t('confirm_new_email_invalid')}</p>\n          )}\n        </Col>\n      </Row>\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Login/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FormEvent, useState, useEffect } from 'react';\nimport { Container, Form, Button, Col } from 'react-bootstrap';\nimport { Link, useNavigate, useSearchParams } from 'react-router-dom';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport type { LoginReqParams, FormDataType } from '@/common/interface';\nimport { Unactivate, WelcomeTitle, PluginRender } from '@/components';\nimport {\n  loggedUserInfoStore,\n  loginSettingStore,\n  userCenterStore,\n} from '@/stores';\nimport {\n  floppyNavigation,\n  guard,\n  handleFormError,\n  userCenter,\n  scrollToElementTop,\n} from '@/utils';\nimport { PluginType, useCaptchaPlugin } from '@/utils/pluginKit';\nimport { login, UcAgent } from '@/services';\nimport { setupAppTheme } from '@/utils/localize';\n\nconst Index: React.FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'login' });\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);\n  const loginSetting = loginSettingStore((state) => state.login);\n  const ucAgent = userCenterStore().agent;\n  let ucAgentInfo: UcAgent['agent_info'] | undefined;\n  if (ucAgent?.enabled && ucAgent?.agent_info) {\n    ucAgentInfo = ucAgent.agent_info;\n  }\n  const canOriginalLogin =\n    (!ucAgentInfo || ucAgentInfo.enabled_original_user_system) &&\n    loginSetting.allow_password_login;\n\n  const [formData, setFormData] = useState<FormDataType>({\n    e_mail: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const [step, setStep] = useState(1);\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const passwordCaptcha = useCaptchaPlugin('password');\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { e_mail, pass } = formData;\n\n    if (!e_mail.value) {\n      bol = false;\n      formData.e_mail = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('email.msg.empty'),\n      };\n    }\n\n    if (!pass.value) {\n      bol = false;\n      formData.pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('password.msg.empty'),\n      };\n    }\n\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const handleLogin = (event?: any) => {\n    if (event) {\n      event.preventDefault();\n    }\n    const params: LoginReqParams = {\n      e_mail: formData.e_mail.value,\n      pass: formData.pass.value,\n    };\n\n    const captcha = passwordCaptcha?.getCaptcha();\n    if (captcha?.verify) {\n      params.captcha_code = captcha.captcha_code;\n      params.captcha_id = captcha.captcha_id;\n    }\n\n    login(params)\n      .then(async (res) => {\n        await passwordCaptcha?.close?.();\n        updateUser(res);\n        setupAppTheme();\n        const userStat = guard.deriveLoginState();\n        if (userStat.isNotActivated) {\n          // inactive\n          setStep(2);\n        } else {\n          guard.handleLoginRedirect(navigate);\n        }\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          passwordCaptcha?.handleCaptchaError?.(err.list);\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = async (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (!passwordCaptcha) {\n      handleLogin();\n      return;\n    }\n\n    passwordCaptcha?.check?.(() => {\n      handleLogin();\n    });\n  };\n\n  useEffect(() => {\n    const isInactive = searchParams.get('status');\n\n    if (storeUser.id && (storeUser.mail_status === 2 || isInactive)) {\n      setStep(2);\n    }\n  }, []);\n\n  usePageTags({\n    title: t('login', { keyPrefix: 'page_title' }),\n  });\n\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>\n      <WelcomeTitle />\n      {step === 1 ? (\n        <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n          <PluginRender\n            type={PluginType.Captcha}\n            slug_name=\"captcha_basic\"\n            className=\"mb-5\"\n          />\n\n          <PluginRender\n            type={PluginType.Captcha}\n            slug_name=\"captcha_google_v2\"\n            className=\"mb-5\"\n          />\n          {ucAgentInfo ? (\n            <PluginRender\n              type={PluginType.Connector}\n              slug_name=\"hosting_connector\"\n              className=\"mb-5\"\n            />\n          ) : (\n            <PluginRender\n              type={PluginType.Connector}\n              slug_name=\"third_party_connector\"\n              className=\"mb-5\"\n            />\n          )}\n          {canOriginalLogin ? (\n            <>\n              <Form noValidate onSubmit={handleSubmit}>\n                <Form.Group controlId=\"email\" className=\"mb-3\">\n                  <Form.Label>{t('email.label')}</Form.Label>\n                  <Form.Control\n                    required\n                    tabIndex={1}\n                    type=\"email\"\n                    value={formData.e_mail.value}\n                    isInvalid={formData.e_mail.isInvalid}\n                    onChange={(e) =>\n                      handleChange({\n                        e_mail: {\n                          value: e.target.value,\n                          isInvalid: false,\n                          errorMsg: '',\n                        },\n                      })\n                    }\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {formData.e_mail.errorMsg}\n                  </Form.Control.Feedback>\n                </Form.Group>\n\n                <Form.Group controlId=\"pass\" className=\"mb-3\">\n                  <div className=\"d-flex justify-content-between\">\n                    <Form.Label>{t('password.label')}</Form.Label>\n                    <Link to=\"/users/account-recovery\" tabIndex={2}>\n                      <small>{t('forgot_pass')}</small>\n                    </Link>\n                  </div>\n\n                  <Form.Control\n                    required\n                    tabIndex={1}\n                    type=\"password\"\n                    // value={formData.pass.value}\n                    isInvalid={formData.pass.isInvalid}\n                    onChange={(e) =>\n                      handleChange({\n                        pass: {\n                          value: e.target.value,\n                          isInvalid: false,\n                          errorMsg: '',\n                        },\n                      })\n                    }\n                  />\n                  <Form.Control.Feedback type=\"invalid\">\n                    {formData.pass.errorMsg}\n                  </Form.Control.Feedback>\n                </Form.Group>\n\n                <div className=\"d-grid\">\n                  <Button variant=\"primary\" type=\"submit\" tabIndex={1}>\n                    {t('login', { keyPrefix: 'btns' })}\n                  </Button>\n                </div>\n              </Form>\n              {loginSetting.allow_new_registrations && (\n                <div className=\"text-center mt-5\">\n                  <Trans i18nKey=\"login.info_sign\" ns=\"translation\">\n                    Don't have an account?\n                    <Link\n                      to={userCenter.getSignUpUrl()}\n                      tabIndex={2}\n                      onClick={floppyNavigation.handleRouteLinkClick}>\n                      Sign up\n                    </Link>\n                  </Trans>\n                </div>\n              )}\n            </>\n          ) : null}\n        </Col>\n      ) : null}\n\n      {step === 2 && <Unactivate visible={step === 2} />}\n    </Container>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Logout/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useEffect } from 'react';\nimport { Spinner } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { logout } from '@/services';\nimport { loggedUserInfoStore } from '@/stores';\nimport Storage from '@/utils/storage';\nimport { RouteAlias, BASE_ORIGIN } from '@/router/alias';\nimport { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'page_title' });\n  const { user: loggedUserInfo, clear: clearUserStore } = loggedUserInfoStore();\n\n  usePageTags({\n    title: t('logout'),\n  });\n\n  useEffect(() => {\n    if (loggedUserInfo.username) {\n      logout().then(() => {\n        clearUserStore();\n        const redirect =\n          Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;\n        Storage.remove(REDIRECT_PATH_STORAGE_KEY);\n        window.location.replace(`${BASE_ORIGIN}${redirect}`);\n      });\n    }\n    // auto height of container\n    const pageWrap = document.querySelector('.page-wrap') as HTMLElement;\n    if (pageWrap) {\n      pageWrap.style.display = 'contents';\n    }\n\n    return () => {\n      if (pageWrap) {\n        pageWrap.style.display = 'block';\n      }\n    };\n  }, []);\n  return (\n    <div className=\"d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center\">\n      <Spinner variant=\"secondary\" />\n    </div>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/components/Achievements/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.achievement-wrap {\n  .num,\n  .icon {\n    width: 60px;\n    flex: none;\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/components/Achievements/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { ListGroup } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\nimport isEmpty from 'lodash/isEmpty';\n\nimport { Empty } from '@/components';\nimport { loggedUserInfoStore } from '@/stores';\n\nimport './index.scss';\n\nconst Achievements = ({ data, handleReadNotification }) => {\n  const { user } = loggedUserInfoStore();\n  const { t } = useTranslation('translation', { keyPrefix: 'notifications' });\n\n  if (!data) {\n    return null;\n  }\n  if (isEmpty(data)) {\n    return <Empty />;\n  }\n  return (\n    <ListGroup className=\"achievement-wrap rounded-0\">\n      {data.map((item) => {\n        const { comment, question, answer } =\n          item?.object_info?.object_map || {};\n        let url = '';\n        switch (item.object_info.object_type) {\n          case 'question':\n            url = `/questions/${item.object_info.object_id}`;\n            break;\n          case 'answer':\n            url = `/questions/${question}/${item.object_info.object_id}`;\n            break;\n          case 'comment':\n            url = `/questions/${question}/${answer}?commentId=${comment}`;\n            break;\n          case 'badge_award':\n            url = `/badges/${item.object_info.object_map.badge_id}?username=${user.username}`;\n            break;\n          default:\n            url = '';\n        }\n        return (\n          <ListGroup.Item\n            key={item.id}\n            className={classNames(\n              'd-flex border-start-0 border-end-0 py-3',\n              !item.is_read && 'warning',\n            )}>\n            {item.object_info.object_type === 'badge_award' ? (\n              <div className=\"icon text-end\">👏</div>\n            ) : (\n              <>\n                {item.rank > 0 && (\n                  <div className=\"text-success num text-end\">{`+${item.rank}`}</div>\n                )}\n                {item.rank === 0 && (\n                  <div className=\"num text-end\">{item.rank}</div>\n                )}\n                {item.rank < 0 && (\n                  <div className=\"text-danger num text-end\">{`${item.rank}`}</div>\n                )}\n              </>\n            )}\n\n            <div className=\"d-flex flex-column ms-3 flex-fill\">\n              <Link to={url} onClick={() => handleReadNotification(item.id)}>\n                {item.object_info.title}\n              </Link>\n              <span className=\"text-secondary small\">\n                {t(item.object_info.object_type)}\n              </span>\n            </div>\n          </ListGroup.Item>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default Achievements;\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/components/Inbox/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { ListGroup } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport classNames from 'classnames';\nimport isEmpty from 'lodash/isEmpty';\n\nimport { FormatTime, Empty } from '@/components';\n\nconst Inbox = ({ data, handleReadNotification }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'notifications' });\n  if (!data) {\n    return null;\n  }\n  if (isEmpty(data)) {\n    return <Empty />;\n  }\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        const { comment, question, answer } =\n          item?.object_info?.object_map || {};\n        let url = '';\n        switch (item.object_info.object_type) {\n          case 'question':\n            url = `/questions/${item.object_info.object_id}`;\n            break;\n          case 'answer':\n            url = `/questions/${question}/${item.object_info.object_id}`;\n            break;\n          case 'comment':\n            url = `/questions/${question}/${answer}?commentId=${comment}`;\n            break;\n          default:\n            url = '';\n        }\n        return (\n          <ListGroup.Item\n            key={item.id}\n            className={classNames(\n              'py-3 border-start-0 border-end-0',\n              !item.is_read && 'warning',\n            )}>\n            <div>\n              {item.user_info && item.user_info.status !== 'deleted' ? (\n                <Link to={`/users/${item.user_info.username}`}>\n                  {item.user_info.display_name}{' '}\n                </Link>\n              ) : (\n                // someone for anonymous user display\n                <span>{item.user_info?.display_name || t('someone')} </span>\n              )}\n              {item.notification_action}{' '}\n              <Link to={url} onClick={() => handleReadNotification(item.id)}>\n                {item.object_info.title}\n              </Link>\n            </div>\n            <div className=\"text-secondary small\">\n              <FormatTime time={item.update_time} />\n            </div>\n          </ListGroup.Item>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default Inbox;\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Inbox from './Inbox';\nimport Achievements from './Achievements';\n\nexport { Inbox, Achievements };\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.inbox-nav {\n  border-top: 1px solid var(--an-answer-item-border-top);\n  padding: 0.5rem 0;\n}\n"
  },
  {
    "path": "ui/src/pages/Users/Notifications/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { Row, Col, ButtonGroup, Button, Nav } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useParams, useNavigate, Link } from 'react-router-dom';\n\nimport classNames from 'classnames';\n\nimport { usePageTags } from '@/hooks';\nimport {\n  useQueryNotifications,\n  clearUnreadNotification,\n  clearNotificationStatus,\n  readNotification,\n} from '@/services';\nimport { floppyNavigation } from '@/utils';\n\nimport { Inbox, Achievements } from './components';\nimport './index.scss';\n\nconst PAGE_SIZE = 10;\n\nconst Notifications = () => {\n  const [page, setPage] = useState(1);\n  const [notificationData, setNotificationData] = useState<any>([]);\n  const { t } = useTranslation('translation', { keyPrefix: 'notifications' });\n  const inboxTypeNavs = ['all', 'posts', 'invites', 'votes'];\n  const { type = 'inbox', subType = inboxTypeNavs[0] } = useParams();\n\n  const queryParams: {\n    type: string;\n    inbox_type?: string;\n    page: number;\n    page_size: number;\n  } = {\n    type,\n    page,\n    page_size: PAGE_SIZE,\n  };\n  if (type === 'inbox') {\n    queryParams.inbox_type = subType;\n  }\n  const { data, mutate } = useQueryNotifications(queryParams);\n\n  useEffect(() => {\n    clearNotificationStatus(type);\n  }, []);\n\n  useEffect(() => {\n    if (!data) {\n      return;\n    }\n    if (page > 1) {\n      setNotificationData([...notificationData, ...(data?.list || [])]);\n    } else {\n      setNotificationData(data?.list);\n    }\n  }, [data]);\n  const navigate = useNavigate();\n\n  const handleTypeChange = (evt, val) => {\n    if (!floppyNavigation.shouldProcessLinkClick(evt)) {\n      return;\n    }\n    evt.preventDefault();\n    if (type === val) {\n      return;\n    }\n    setPage(1);\n    setNotificationData([]);\n    navigate(`/users/notifications/${val}`);\n  };\n\n  const handleLoadMore = () => {\n    setPage(page + 1);\n  };\n\n  const handleUnreadNotification = async () => {\n    await clearUnreadNotification(type);\n    mutate();\n  };\n\n  const handleReadNotification = (id) => {\n    readNotification(id);\n  };\n  usePageTags({\n    title: t('notifications', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Row className=\"pt-4 mb-5\">\n      <Col className=\"page-main flex-auto\">\n        <h3 className=\"mb-4\">{t('title')}</h3>\n        <div className=\"d-flex justify-content-between mb-3\">\n          <ButtonGroup size=\"sm\">\n            <Button\n              as=\"a\"\n              href=\"/users/notifications/inbox\"\n              variant=\"outline-secondary\"\n              active={type === 'inbox'}\n              onClick={(evt) => handleTypeChange(evt, 'inbox')}>\n              {t('inbox')}\n            </Button>\n            <Button\n              as=\"a\"\n              href=\"/users/notifications/achievement\"\n              variant=\"outline-secondary\"\n              active={type === 'achievement'}\n              onClick={(evt) => handleTypeChange(evt, 'achievement')}>\n              {t('achievement')}\n            </Button>\n          </ButtonGroup>\n          <Button\n            size=\"sm\"\n            variant=\"outline-secondary\"\n            onClick={handleUnreadNotification}>\n            {t('all_read')}\n          </Button>\n        </div>\n        {type === 'inbox' && (\n          <>\n            <Nav className=\"inbox-nav small\">\n              {inboxTypeNavs.map((nav) => {\n                const navLinkHref = `/users/notifications/inbox/${nav}`;\n                const navLinkName = t(`inbox_type.${nav}`);\n                return (\n                  <Nav.Item key={nav}>\n                    <Link\n                      to={navLinkHref}\n                      onClick={() => {\n                        setPage(1);\n                      }}\n                      className={classNames('nav-link', {\n                        disabled: nav === subType,\n                      })}>\n                      {navLinkName}\n                    </Link>\n                  </Nav.Item>\n                );\n              })}\n            </Nav>\n            <Inbox\n              data={notificationData}\n              handleReadNotification={handleReadNotification}\n            />\n          </>\n        )}\n        {type === 'achievement' && (\n          <Achievements\n            data={notificationData}\n            handleReadNotification={handleReadNotification}\n          />\n        )}\n        {(data?.count || 0) > PAGE_SIZE * page && (\n          <div className=\"d-flex justify-content-center align-items-center py-3\">\n            <Button\n              variant=\"link\"\n              className=\"btn-no-border\"\n              onClick={handleLoadMore}>\n              {t('show_more')}\n            </Button>\n          </div>\n        )}\n      </Col>\n      <Col className=\"page-right-side\" />\n    </Row>\n  );\n};\n\nexport default Notifications;\n"
  },
  {
    "path": "ui/src/pages/Users/OauthBindEmail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useState, useEffect } from 'react';\nimport { Container, Col, Form, Button } from 'react-bootstrap';\nimport { useTranslation, Trans } from 'react-i18next';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\n\nimport { Modal, WelcomeTitle } from '@/components';\nimport type { FormDataType } from '@/common/interface';\nimport { usePageTags } from '@/hooks';\nimport { loggedUserInfoStore } from '@/stores';\nimport { oAuthBindEmail, getLoggedUserInfo } from '@/services';\nimport Storage from '@/utils/storage';\nimport { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'oauth_bind_email',\n  });\n  const navigate = useNavigate();\n  const [searchParams, setUrlSearchParams] = useSearchParams();\n  const updateUser = loggedUserInfoStore((state) => state.update);\n  const binding_key = searchParams.get('binding_key') || '';\n  const [showResult, setShowResult] = useState(false);\n\n  usePageTags({\n    title: t('confirm_email', { keyPrefix: 'page_title' }),\n  });\n  const [formData, setFormData] = useState<FormDataType>({\n    email: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n\n    if (!formData.email.value) {\n      bol = false;\n      formData.email = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('email.msg.empty'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const ele = document.getElementById('email');\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const getUserInfo = (token) => {\n    Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);\n    getLoggedUserInfo().then((user) => {\n      updateUser(user);\n      setTimeout(() => {\n        navigate('/users/login?status=inactive', { replace: true });\n      }, 0);\n    });\n  };\n\n  const connectConfirm = () => {\n    Modal.confirm({\n      title: t('modal_title'),\n      content: t('modal_content'),\n      cancelText: t('modal_cancel'),\n      confirmText: t('modal_confirm'),\n      onConfirm: () => {\n        // send activation email\n        oAuthBindEmail({\n          binding_key,\n          email: formData.email.value,\n          must: true,\n        }).then((result) => {\n          if (result.access_token) {\n            getUserInfo(result.access_token);\n          } else {\n            searchParams.delete('binding_key');\n            setUrlSearchParams('');\n            setShowResult(true);\n          }\n        });\n      },\n      onCancel: () => {\n        setFormData({\n          email: {\n            value: '',\n            isInvalid: false,\n            errorMsg: '',\n          },\n        });\n      },\n    });\n  };\n\n  const handleSubmit = (event: any) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n\n    if (binding_key) {\n      oAuthBindEmail({\n        binding_key,\n        email: formData.email.value,\n        must: false,\n      })\n        .then((res) => {\n          if (res.email_exist_and_must_be_confirmed) {\n            connectConfirm();\n          }\n          if (res.access_token) {\n            getUserInfo(res.access_token);\n          }\n        })\n        .catch((err) => {\n          if (err.isError) {\n            const data = handleFormError(err, formData);\n            setFormData({ ...data });\n            const ele = document.getElementById(err.list[0].error_field);\n            scrollToElementTop(ele);\n          }\n        });\n    }\n  };\n\n  useEffect(() => {\n    if (!binding_key) {\n      navigate('/', { replace: true });\n    }\n  }, []);\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>\n      <WelcomeTitle />\n      {showResult ? (\n        <Col md={6} className=\"mx-auto text-center\">\n          <p>\n            <Trans\n              i18nKey=\"inactive.first\"\n              values={{ mail: formData.email.value }}\n              components={{ bold: <strong /> }}\n            />\n          </p>\n          <p>{t('info', { keyPrefix: 'inactive' })}</p>\n        </Col>\n      ) : (\n        <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n          <div className=\"text-center mb-5\">{t('subtitle')}</div>\n          <Form noValidate onSubmit={handleSubmit} autoComplete=\"off\">\n            <Form.Group controlId=\"email\" className=\"mb-3\">\n              <Form.Label>{t('email.label')}</Form.Label>\n              <Form.Control\n                required\n                type=\"email\"\n                value={formData.email.value}\n                isInvalid={formData.email.isInvalid}\n                onChange={(e) => {\n                  handleChange({\n                    email: {\n                      value: e.target.value,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.email.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <div className=\"d-grid mb-3\">\n              <Button variant=\"primary\" type=\"submit\">\n                {t('btn_update')}\n              </Button>\n            </div>\n          </Form>\n        </Col>\n      )}\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/PasswordReset/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FormEvent, useState } from 'react';\nimport { Container, Col, Form, Button } from 'react-bootstrap';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePageTags } from '@/hooks';\nimport { loggedUserInfoStore } from '@/stores';\nimport type { FormDataType } from '@/common/interface';\nimport { replacementPassword } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Index: React.FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });\n  const [searchParams] = useSearchParams();\n  const [step, setStep] = useState(1);\n  const clearUser = loggedUserInfoStore((state) => state.clear);\n  const [formData, setFormData] = useState<FormDataType>({\n    pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    passSecond: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { pass, passSecond } = formData;\n\n    if (!pass.value) {\n      bol = false;\n      formData.pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('password.msg.empty'),\n      };\n    }\n\n    if (bol && pass.value && pass.value.length < 8) {\n      bol = false;\n      formData.pass = {\n        value: pass.value,\n        isInvalid: true,\n        errorMsg: t('password.msg.length'),\n      };\n    }\n\n    if (bol && !passSecond.value) {\n      bol = false;\n      formData.passSecond = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('password.msg.empty'),\n      };\n    }\n\n    if (bol && passSecond.value && passSecond.value.length < 8) {\n      bol = false;\n      formData.passSecond = {\n        value: passSecond.value,\n        isInvalid: true,\n        errorMsg: t('password.msg.length'),\n      };\n    }\n\n    if (bol && pass.value !== passSecond.value) {\n      bol = false;\n      formData.passSecond = {\n        value: passSecond.value,\n        isInvalid: true,\n        errorMsg: t('password.msg.different'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n    return bol;\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (checkValidated() === false) {\n      return;\n    }\n    const code = searchParams.get('code');\n    if (!code) {\n      console.error('code is required');\n      return;\n    }\n    replacementPassword({\n      code: encodeURIComponent(code),\n      pass: formData.pass.value,\n    })\n      .then(() => {\n        // clear login information then to login page\n        clearUser();\n        setStep(2);\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n  usePageTags({\n    title: t('account_recovery', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>\n      <h3 className=\"text-center mb-5\">{t('page_title')}</h3>\n      {step === 1 && (\n        <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n          <Form noValidate onSubmit={handleSubmit} autoComplete=\"off\">\n            <Form.Group controlId=\"pass\" className=\"mb-3\">\n              <Form.Label>{t('password.label')}</Form.Label>\n              <Form.Control\n                autoComplete=\"off\"\n                required\n                type=\"password\"\n                isInvalid={formData.pass.isInvalid}\n                onChange={(e) => {\n                  handleChange({\n                    pass: {\n                      value: e.target.value,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.pass.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <Form.Group controlId=\"passSecond\" className=\"mb-3\">\n              <Form.Label>{t('password_confirm.label')}</Form.Label>\n              <Form.Control\n                autoComplete=\"off\"\n                required\n                type=\"password\"\n                isInvalid={formData.passSecond.isInvalid}\n                onChange={(e) => {\n                  handleChange({\n                    passSecond: {\n                      value: e.target.value,\n                      isInvalid: false,\n                      errorMsg: '',\n                    },\n                  });\n                }}\n              />\n              <Form.Control.Feedback type=\"invalid\">\n                {formData.passSecond.errorMsg}\n              </Form.Control.Feedback>\n            </Form.Group>\n\n            <div className=\"d-grid mb-3\">\n              <Button variant=\"primary\" type=\"submit\">\n                {t('btn_name')}\n              </Button>\n            </div>\n          </Form>\n        </Col>\n      )}\n\n      {step === 2 && (\n        <Col className=\"mx-auto px-4\" md={6}>\n          <div className=\"text-center\">\n            <p>{t('reset_success')}</p>\n            <Link to=\"/users/login\">{t('to_login')}</Link>\n          </div>\n        </Col>\n      )}\n\n      {step === 3 && (\n        <Col className=\"mx-auto px-4\" md={6}>\n          <div className=\"text-center\">\n            <p>{t('link_invalid')}</p>\n            <Link to=\"/users/login\">{t('to_login')}</Link>\n          </div>\n        </Col>\n      )}\n    </Container>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Alert/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC, useState } from 'react';\nimport { Alert } from 'react-bootstrap';\n\ninterface Props {\n  data;\n}\nconst Index: FC<Props> = ({ data }) => {\n  const [show, setShow] = useState(Boolean(data));\n\n  return (\n    <Alert\n      variant=\"info\"\n      show={show}\n      className=\"mb-3\"\n      dismissible\n      onClose={() => {\n        setShow(false);\n      }}>\n      <div dangerouslySetInnerHTML={{ __html: data }} />\n    </Alert>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Answers/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\n\nimport { FormatTime, Tag, Counts } from '@/components';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  visible: boolean;\n  data: any[];\n}\nconst Index: FC<Props> = ({ visible, data }) => {\n  if (!visible || !data?.length) {\n    return null;\n  }\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        return (\n          <ListGroupItem\n            className=\"py-3 px-0 bg-transparent border-start-0 border-end-0\"\n            key={item.answer_id}>\n            <h6 className=\"mb-2\">\n              <Link\n                to={pathFactory.answerLanding({\n                  questionId: item.question_id,\n                  slugTitle: item.question_info?.url_title,\n                  answerId: item.answer_id,\n                })}\n                className=\"text-break\">\n                {item.question_info?.title}\n              </Link>\n            </h6>\n            <div className=\"d-flex align-items-center small text-secondary mb-2\">\n              <FormatTime time={item.create_time} className=\"me-3\" />\n\n              <Counts\n                data={{ votes: item?.vote_count, views: 0, answers: 0 }}\n                showAnswers={false}\n                showViews={false}\n                showAccepted={item.accepted === 2}\n              />\n            </div>\n            <div>\n              {item.question_info?.tags?.map((tag) => {\n                return <Tag key={tag.slug_name} className=\"me-1\" data={tag} />;\n              })}\n            </div>\n          </ListGroupItem>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Badges/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Row, Col } from 'react-bootstrap';\n\nimport * as Type from '@/common/interface';\nimport { CardBadge } from '@/components';\n\ninterface IProps {\n  data: Type.BadgeListItem[];\n  username: string;\n  visible: boolean;\n}\n\nconst Index: FC<IProps> = ({ data, visible, username }) => {\n  if (!visible) {\n    return null;\n  }\n  return (\n    <Row>\n      {data.map((item) => {\n        return (\n          <Col sm={6} md={4} lg={3} key={item.id} className=\"mb-4\">\n            <CardBadge\n              data={item}\n              urlSearchParams={`username=${username}`}\n              badgePillType=\"count\"\n            />\n          </Col>\n        );\n      })}\n    </Row>\n  );\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Comments/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport { FormatTime } from '@/components';\n\ninterface Props {\n  visible: boolean;\n  data;\n}\n\nconst Index: FC<Props> = ({ visible, data }) => {\n  if (!visible || !data?.length) {\n    return null;\n  }\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        return (\n          <ListGroupItem\n            className=\"py-3 px-0 bg-transparent border-start-0 border-end-0\"\n            key={item.comment_id}>\n            <Link\n              className=\"text-break\"\n              to={\n                item.object_type === 'question'\n                  ? pathFactory.questionLanding(\n                      item.question_id,\n                      item.url_title,\n                    )\n                  : pathFactory.answerLanding({\n                      questionId: item.question_id,\n                      slugTitle: item.url_title,\n                      answerId: item.answer_id,\n                    })\n              }>\n              {item.title}\n            </Link>\n            <div\n              className=\"small mb-2 last-p text-break text-truncate-2\"\n              dangerouslySetInnerHTML={{\n                __html: item.content,\n              }}\n            />\n\n            <FormatTime\n              time={item.created_at}\n              className=\"small text-secondary\"\n            />\n          </ListGroupItem>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/DefaultList/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { FormatTime, Tag, BaseUserCard, Counts } from '@/components';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  visible: boolean;\n  tabName: string;\n  data: any[];\n}\n\nconst Index: FC<Props> = ({ visible, tabName, data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  if (!visible) {\n    return null;\n  }\n\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        return (\n          <ListGroupItem\n            className=\"py-3 px-0 bg-transparent border-start-0 border-end-0\"\n            key={tabName === 'questions' ? item.question_id : item.id}>\n            <h6 className=\"mb-2\">\n              <Link\n                className=\"text-break\"\n                to={pathFactory.questionLanding(\n                  tabName === 'questions' ? item.question_id : item.id,\n                  item.url_title,\n                )}>\n                {item.title}\n                {tabName === 'questions' && item.status === 'closed'\n                  ? ` [${t('closed', { keyPrefix: 'question' })}]`\n                  : null}\n              </Link>\n            </h6>\n            <div className=\"d-flex flex-wrap align-items-center small text-secondary mb-2\">\n              {tabName === 'bookmarks' && (\n                <>\n                  <BaseUserCard data={item.user_info} showAvatar={false} />\n                  <span className=\"split-dot\" />\n                </>\n              )}\n\n              <FormatTime\n                time={\n                  tabName === 'bookmarks' ? item.create_time : item.created_at\n                }\n                className=\"me-3\"\n              />\n\n              <Counts\n                isAccepted={Number(item.accepted_answer_id) > 0}\n                data={{\n                  votes: item.vote_count,\n                  answers: item.answer_count,\n                  views: item.view_count,\n                }}\n              />\n            </div>\n            <div>\n              {item.tags?.map((tag) => {\n                return <Tag className=\"me-1\" key={tag.slug_name} data={tag} />;\n              })}\n            </div>\n          </ListGroupItem>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/ListHead/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { QueryGroup } from '@/components';\n\nconst sortBtns = ['newest', 'score'];\n\ninterface Props {\n  tabName: string;\n  count: number;\n  sort: string;\n  visible: boolean;\n}\nconst Index: FC<Props> = ({\n  tabName = 'answers',\n  visible,\n  sort,\n  count = 0,\n}) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n\n  if (!visible) {\n    return null;\n  }\n\n  return (\n    <div className=\"d-flex  align-items-center justify-content-between pb-3\">\n      <h5 className=\"mb-0\">\n        {count} {t(tabName)}\n      </h5>\n      {(tabName === 'answers' || tabName === 'questions') && (\n        <QueryGroup\n          data={sortBtns}\n          currentSort={sort}\n          i18nKeyPrefix=\"personal\"\n        />\n      )}\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/NavBar/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Nav } from 'react-bootstrap';\nimport { NavLink } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\ninterface Props {\n  slug: string;\n  isSelf: boolean;\n  tabName: string;\n}\n\nconst list = [\n  {\n    path: '',\n    name: 'overview',\n  },\n  {\n    path: '/answers',\n    name: 'answers',\n  },\n  {\n    path: '/questions',\n    name: 'questions',\n  },\n  {\n    role: 'self', // Only visible to author\n    path: '/bookmarks',\n    name: 'bookmarks',\n  },\n  {\n    path: '/reputation',\n    name: 'reputation',\n  },\n  {\n    path: '/comments',\n    name: 'comments',\n  },\n  {\n    role: 'self', // Only visible to author\n    path: '/votes',\n    name: 'votes',\n  },\n  {\n    path: '/badges',\n    name: 'badges',\n  },\n];\nconst Index: FC<Props> = ({ slug, tabName = 'overview', isSelf }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  return (\n    <Nav\n      className=\"pt-2 mb-4 flex-nowrap\"\n      variant=\"pills\"\n      style={{ overflow: 'auto' }}>\n      {list.map((item) => {\n        if (item.role && !isSelf) {\n          return null;\n        }\n        if (item.path) {\n          return (\n            <NavLink\n              to={`/users/${slug}${item.path}`}\n              key={item.name}\n              className=\"nav-link\">\n              {t(item.name)}\n            </NavLink>\n          );\n        }\n        return (\n          <NavLink\n            key={item.name}\n            to={`/users/${slug}`}\n            className={({ isActive }) =>\n              isActive && tabName === 'overview'\n                ? 'nav-link active'\n                : 'nav-link'\n            }>\n            {t(item.name)}\n          </NavLink>\n        );\n      })}\n    </Nav>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Overview/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Row, Col } from 'react-bootstrap';\n\n// import * as Type from '@/common/interface';\nimport { CardBadge } from '@/components';\nimport { useGetRecentAwardBadges } from '@/services';\nimport TopList from '../TopList';\n\ninterface Props {\n  username: string;\n  visible: boolean;\n  introduction: string;\n  data;\n}\nconst Index: FC<Props> = ({ visible, introduction, data, username }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  const { data: recentBadges } = useGetRecentAwardBadges(\n    visible ? username : null,\n  );\n  if (!visible) {\n    return null;\n  }\n  return (\n    <div>\n      <h5 className=\"mb-3\">{t('about_me')}</h5>\n      {introduction ? (\n        <div\n          className=\"mb-5 text-break fmt\"\n          dangerouslySetInnerHTML={{ __html: introduction }}\n        />\n      ) : (\n        <div className=\"mb-5\">{t('about_me_empty')}</div>\n      )}\n\n      <Row className=\"mb-4\">\n        <Col sm={12} md={6} className=\"mb-4\">\n          <h5 className=\"mb-3\">{t('top_answers')}</h5>\n          {data?.answer?.length > 0 ? (\n            <TopList data={data?.answer} type=\"answer\" />\n          ) : (\n            <div className=\"mb-5\">{t('content_empty')}</div>\n          )}\n        </Col>\n        <Col sm={12} md={6}>\n          <h5 className=\"mb-3\">{t('top_questions')}</h5>\n          {data?.question?.length > 0 ? (\n            <TopList data={data?.question} type=\"question\" />\n          ) : (\n            <div className=\"mb-5\">{t('content_empty')}</div>\n          )}\n        </Col>\n      </Row>\n\n      <div className=\"mb-4\">\n        <h5 className=\"mb-3\">{t('recent_badges')}</h5>\n        {Number(recentBadges?.count) > 0 ? (\n          <Row>\n            {recentBadges?.list?.map((item) => {\n              return (\n                <Col sm={6} md={4} lg={3} key={item.id} className=\"mb-4\">\n                  <CardBadge\n                    data={item}\n                    urlSearchParams={`username=${username}`}\n                    badgePillType=\"count\"\n                  />\n                </Col>\n              );\n            })}\n          </Row>\n        ) : (\n          <div className=\"mb-5\">{t('content_empty')}</div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Reputation/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\n\nimport { FormatTime } from '@/components';\nimport { pathFactory } from '@/router/pathFactory';\n\ninterface Props {\n  visible: boolean;\n  data;\n}\n\nconst Index: FC<Props> = ({ visible, data }) => {\n  if (!visible || !data?.length) {\n    return null;\n  }\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        return (\n          <ListGroupItem\n            className=\"d-flex py-3 px-0 bg-transparent border-start-0 border-end-0\"\n            key={item.object_id}>\n            <div\n              className={`me-3 text-end ${\n                item.reputation > 0 ? 'text-success' : 'text-danger'\n              }`}\n              style={{ width: '40px', minWidth: '40px' }}>\n              {item.reputation > 0 ? '+' : ''}\n              {item.reputation}\n            </div>\n            <div>\n              <Link\n                className=\"text-break\"\n                to={\n                  item.object_type === 'question'\n                    ? pathFactory.questionLanding(\n                        item.question_id,\n                        item.url_title,\n                      )\n                    : pathFactory.answerLanding({\n                        questionId: item.question_id,\n                        slugTitle: item.url_title,\n                        answerId: item.answer_id,\n                      })\n                }>\n                {item.title}\n              </Link>\n              <div className=\"d-flex align-items-center small text-secondary\">\n                <span>{item.rank_type}</span>\n                <span className=\"split-dot\" />\n                <FormatTime time={item.created_at} className=\"me-4\" />\n              </div>\n            </div>\n          </ListGroupItem>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/TopList/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport { Icon } from '@/components';\n\ninterface Props {\n  data: any[];\n  type: 'answer' | 'question';\n}\nconst Index: FC<Props> = ({ data, type }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  return (\n    <ol className=\"list-unstyled\">\n      {data?.map((item, index) => {\n        return (\n          <li\n            className={`${index === data.length - 1 ? '' : 'mb-2'}`}\n            key={type === 'answer' ? item.answer_id : item.question_id}>\n            <Link\n              className=\"text-truncate-1\"\n              to={\n                type === 'answer'\n                  ? pathFactory.answerLanding({\n                      questionId: item.question_id,\n                      slugTitle: item.question_info?.url_title,\n                      answerId: item.answer_id,\n                    })\n                  : pathFactory.questionLanding(\n                      item.question_id,\n                      item.url_title,\n                    )\n              }>\n              {type === 'answer' ? item.question_info.title : item.title}\n            </Link>\n\n            <div className=\"text-secondary small\">\n              <Icon name=\"hand-thumbs-up-fill me-1\" />\n              <span>\n                {item.vote_count} {t('votes', { keyPrefix: 'counts' })}\n              </span>\n\n              {type === 'question' && (\n                <div\n                  className={`d-inline-block text-secondary ms-3 small ${\n                    Number(item.accepted_answer_id) > 0 ? 'text-success' : ''\n                  }`}>\n                  {Number(item.accepted_answer_id) > 0 ? (\n                    <Icon name=\"check-circle-fill\" />\n                  ) : (\n                    <Icon name=\"chat-square-text-fill\" />\n                  )}\n\n                  <span>\n                    {' '}\n                    {item.answer_count} {t('answers', { keyPrefix: 'counts' })}\n                  </span>\n                </div>\n              )}\n\n              {type === 'answer' && item.accepted === 2 && (\n                <div className=\"d-inline-block text-success ms-3 small\">\n                  <Icon name=\"check-circle-fill\" />\n                  <span> {t('accepted')}</span>\n                </div>\n              )}\n            </div>\n          </li>\n        );\n      })}\n    </ol>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/UserInfo/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect, useState } from 'react';\nimport { OverlayTrigger, Tooltip } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport classnames from 'classnames';\n\nimport { Avatar, Icon, SvgIcon } from '@/components';\nimport type { UserInfoRes } from '@/common/interface';\nimport { getUcBranding, UcBrandingEntry } from '@/services';\nimport { userCenterStore } from '@/stores';\n\ninterface Props {\n  data: UserInfoRes;\n}\n\nconst Index: FC<Props> = ({ data }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  const { agent: ucAgent } = userCenterStore();\n  const [ucBranding, setUcBranding] = useState<UcBrandingEntry[]>([]);\n\n  const initData = () => {\n    if (ucAgent?.enabled && data?.username) {\n      getUcBranding(data.username).then((resp) => {\n        if (resp.enabled && Array.isArray(resp.personal_branding)) {\n          setUcBranding(resp.personal_branding);\n        }\n      });\n    }\n  };\n\n  useEffect(() => {\n    initData();\n  }, [data?.username]);\n  if (!data?.username) {\n    return null;\n  }\n  return (\n    <div className=\"d-flex flex-column flex-md-row mb-4\">\n      {data?.status !== 'deleted' ? (\n        <Link to={`/users/${data.username}`} reloadDocument>\n          <Avatar\n            avatar={data.avatar}\n            size=\"160px\"\n            searchStr=\"s=256\"\n            alt={data.display_name}\n          />\n        </Link>\n      ) : (\n        <Avatar\n          avatar={data.avatar}\n          size=\"160px\"\n          searchStr=\"s=256\"\n          alt={data.display_name}\n        />\n      )}\n\n      <div className=\"ms-0 ms-md-4 mt-4 mt-md-0\">\n        <div className=\"d-flex align-items-center mb-2\">\n          {data?.status !== 'deleted' ? (\n            <Link\n              to={`/users/${data.username}`}\n              className=\"link-dark h3 mb-0\"\n              reloadDocument>\n              {data.display_name}\n            </Link>\n          ) : (\n            <span className=\"link-dark h3 mb-0\">{data.display_name}</span>\n          )}\n          {data?.role_id === 2 && (\n            <div className=\"ms-2\">\n              <OverlayTrigger\n                placement=\"top\"\n                overlay={<Tooltip>{t('mod_long')}</Tooltip>}>\n                <span className=\"badge text-bg-light\">{t('mod_short')}</span>\n              </OverlayTrigger>\n            </div>\n          )}\n        </div>\n        <div className=\"text-secondary mb-4\">@{data.username}</div>\n\n        <div className=\"d-flex flex-wrap mb-3\">\n          <div className=\"me-3\">\n            <strong className=\"fs-5\">{data.rank || 0}</strong>\n            <span className=\"text-secondary\"> {t('x_reputation')}</span>\n          </div>\n          <div className=\"me-3\">\n            <strong className=\"fs-5\">{data.answer_count || 0}</strong>\n            <span className=\"text-secondary\"> {t('x_answers')}</span>\n          </div>\n          <div>\n            <strong className=\"fs-5\">{data?.question_count || 0}</strong>\n            <span className=\"text-secondary\"> {t('x_questions')}</span>\n          </div>\n        </div>\n\n        <div className=\"d-flex text-secondary\">\n          {!ucAgent?.enabled ? (\n            <>\n              {data.location && (\n                <div className=\"d-flex align-items-center me-3\">\n                  <Icon name=\"geo-alt-fill\" className=\"me-2\" />\n                  <span>{data.location}</span>\n                </div>\n              )}\n              {data.website && (\n                <div className=\"d-flex align-items-center\">\n                  <Icon name=\"house-door-fill\" className=\"me-2\" />\n                  <a\n                    className=\"link-secondary\"\n                    href={\n                      data.website?.startsWith('http')\n                        ? data.website\n                        : `http://${data.website}`\n                    }>\n                    {\n                      data?.website\n                        .replace(/(http|https):\\/\\//, '')\n                        .split('/')?.[0]\n                    }\n                  </a>\n                </div>\n              )}\n            </>\n          ) : null}\n          {ucBranding.map((b, i, a) => {\n            if (!b.label) {\n              return null;\n            }\n            return (\n              <div\n                key={b.name}\n                className={classnames('d-flex', 'align-items-center', {\n                  'me-3': i < a.length - 1,\n                })}>\n                {b.icon ? (\n                  <SvgIcon base64={b.icon} svgClassName=\"me-2\" />\n                ) : null}\n                {b.url ? (\n                  <a className=\"link-secondary\" href={b.url}>\n                    {b.label}\n                  </a>\n                ) : (\n                  <span>{b.label}</span>\n                )}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/Votes/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { ListGroup, ListGroupItem } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\n\nimport { pathFactory } from '@/router/pathFactory';\nimport { FormatTime } from '@/components';\n\ninterface Props {\n  visible: boolean;\n  data: any[];\n}\n\nconst Index: FC<Props> = ({ visible, data }) => {\n  if (!visible || !data?.length) {\n    return null;\n  }\n\n  return (\n    <ListGroup className=\"rounded-0\">\n      {data.map((item) => {\n        return (\n          <ListGroupItem\n            className=\"d-flex py-3 px-0 bg-transparent border-start-0 border-end-0\"\n            key={item.object_id}>\n            <div\n              className=\"me-3 text-end text-secondary flex-shrink-0\"\n              style={{ width: '80px' }}>\n              {item.vote_type}\n            </div>\n            <div>\n              <Link\n                className=\"text-break\"\n                to={\n                  item.object_type === 'question'\n                    ? pathFactory.questionLanding(\n                        item.question_id,\n                        item.url_title,\n                      )\n                    : pathFactory.answerLanding({\n                        questionId: item.question_id,\n                        slugTitle: item.url_title,\n                        answerId: item.answer_id,\n                      })\n                }>\n                {item.title}\n              </Link>\n              <div className=\"d-flex align-items-center small text-secondary\">\n                <span>{item.object_type}</span>\n\n                <span className=\"split-dot\" />\n                <FormatTime time={item.created_at} className=\"me-4\" />\n              </div>\n            </div>\n          </ListGroupItem>\n        );\n      })}\n    </ListGroup>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport UserInfo from './UserInfo';\nimport NavBar from './NavBar';\nimport Overview from './Overview';\nimport TopList from './TopList';\nimport Alert from './Alert';\nimport ListHead from './ListHead';\nimport DefaultList from './DefaultList';\nimport Reputation from './Reputation';\nimport Comments from './Comments';\nimport Votes from './Votes';\nimport Answers from './Answers';\nimport Badges from './Badges';\n\nexport {\n  Alert,\n  UserInfo,\n  NavBar,\n  Overview,\n  TopList,\n  ListHead,\n  DefaultList,\n  Reputation,\n  Comments,\n  Votes,\n  Answers,\n  Badges,\n};\n"
  },
  {
    "path": "ui/src/pages/Users/Personal/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { useParams, useSearchParams, Link } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { Pagination, FormatTime, Empty } from '@/components';\nimport { loggedUserInfoStore } from '@/stores';\nimport {\n  usePersonalInfoByName,\n  usePersonalTop,\n  usePersonalListByTabName,\n} from '@/services';\nimport type { UserInfoRes } from '@/common/interface';\n\nimport {\n  UserInfo,\n  NavBar,\n  Overview,\n  Alert,\n  ListHead,\n  DefaultList,\n  Reputation,\n  Comments,\n  Answers,\n  Votes,\n  Badges,\n} from './components';\n\nconst Personal: FC = () => {\n  const { tabName = 'overview', username = '' } = useParams();\n  const [searchParams] = useSearchParams();\n  const page = searchParams.get('page') || 1;\n  const order = searchParams.get('order') || 'newest';\n  const { t } = useTranslation('translation', { keyPrefix: 'personal' });\n  const sessionUser = loggedUserInfoStore((state) => state.user);\n  const isSelf = sessionUser?.username === username;\n\n  const { data: userInfo } = usePersonalInfoByName(username);\n  const { data: topData } = usePersonalTop(username, tabName);\n\n  const { data: listData, isLoading = true } = usePersonalListByTabName(\n    {\n      username,\n      page: Number(page),\n      page_size: 30,\n      order,\n    },\n    tabName,\n  );\n  const { count = 0, list = [] } = listData?.[tabName] || {};\n\n  let pageTitle = '';\n  if (userInfo?.username) {\n    pageTitle = `${userInfo?.display_name} (${userInfo?.username})`;\n  }\n  usePageTags({\n    title: pageTitle,\n  });\n\n  return (\n    <div className=\"pt-4 mb-5\">\n      <Row>\n        <Col>\n          {userInfo?.status !== 'normal' && userInfo?.status_msg && (\n            <Alert data={userInfo?.status_msg} />\n          )}\n          <div className=\"d-md-flex d-block flex-wrap justify-content-between\">\n            <UserInfo data={userInfo as UserInfoRes} />\n            {isSelf && (\n              <div className=\"mb-3\">\n                <Link\n                  className=\"btn btn-outline-secondary\"\n                  to=\"/users/settings/profile\">\n                  {t('edit_profile')}\n                </Link>\n              </div>\n            )}\n          </div>\n          <NavBar tabName={tabName} slug={username} isSelf={isSelf} />\n\n          <Overview\n            visible={tabName === 'overview'}\n            introduction={userInfo?.bio_html || ''}\n            data={topData}\n            username={username}\n          />\n\n          <ListHead\n            count={tabName === 'reputation' ? Number(userInfo?.rank) : count}\n            sort={order}\n            visible={tabName !== 'overview'}\n            tabName={tabName}\n          />\n          <Answers data={list} visible={tabName === 'answers'} />\n          <DefaultList\n            data={list}\n            tabName={tabName}\n            visible={tabName === 'questions' || tabName === 'bookmarks'}\n          />\n          <Reputation data={list} visible={tabName === 'reputation'} />\n          <Comments data={list} visible={tabName === 'comments'} />\n          <Votes data={list} visible={tabName === 'votes'} />\n          <Badges\n            data={list}\n            visible={tabName === 'badges'}\n            username={username}\n          />\n          {!list?.length && !isLoading && <Empty />}\n\n          {count > 0 && (\n            <div className=\"d-flex justify-content-center py-4\">\n              <Pagination\n                pageSize={30}\n                totalSize={count || 0}\n                currentPage={Number(page)}\n              />\n            </div>\n          )}\n\n          {tabName === 'overview' && (\n            <>\n              <h5 className=\"mb-3\">{t('stats')}</h5>\n              {userInfo?.created_at && (\n                <div className=\"text-secondary\">\n                  <FormatTime time={userInfo.created_at} preFix={t('joined')} />\n                  {t('comma')}{' '}\n                  <FormatTime\n                    time={userInfo.last_login_date}\n                    preFix={t('last_login')}\n                  />\n                </div>\n              )}\n            </>\n          )}\n        </Col>\n      </Row>\n    </div>\n  );\n};\nexport default Personal;\n"
  },
  {
    "path": "ui/src/pages/Users/Register/components/SignUpForm/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FormEvent, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { Link } from 'react-router-dom';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport type { FormDataType, RegisterReqParams } from '@/common/interface';\nimport { register } from '@/services';\nimport userStore from '@/stores/loggedUserInfo';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { useLegalClick } from '@/behaviour/useLegalClick';\n\ninterface Props {\n  callback: () => void;\n}\n\nconst Index: React.FC<Props> = ({ callback }) => {\n  const { t } = useTranslation('translation', { keyPrefix: 'login' });\n  const [formData, setFormData] = useState<FormDataType>({\n    name: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    e_mail: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const updateUser = userStore((state) => state.update);\n  const emailCaptcha = useCaptchaPlugin('email');\n  const nameRegex = /^[\\w.-\\s]{2,30}$/;\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { name, e_mail, pass } = formData;\n\n    if (!name.value) {\n      bol = false;\n      formData.name = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('name.msg.empty'),\n      };\n    } else if (name.value.length < 2 || name.value.length > 30) {\n      bol = false;\n      formData.name = {\n        value: name.value,\n        isInvalid: true,\n        errorMsg: t('name.msg.range'),\n      };\n    } else if (!nameRegex.test(name.value)) {\n      bol = false;\n      formData.name = {\n        value: name.value,\n        isInvalid: true,\n        errorMsg: t('name.msg.character'),\n      };\n    }\n\n    if (!e_mail.value) {\n      bol = false;\n      formData.e_mail = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('email.msg.empty'),\n      };\n    }\n\n    if (!pass.value) {\n      bol = false;\n      formData.pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('password.msg.empty'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n    return bol;\n  };\n\n  const legalClick = useLegalClick();\n\n  const handleRegister = (event?: any) => {\n    if (event) {\n      event.preventDefault();\n    }\n    const reqParams: RegisterReqParams = {\n      name: formData.name.value,\n      e_mail: formData.e_mail.value,\n      pass: formData.pass.value,\n    };\n\n    const captcha = emailCaptcha?.getCaptcha();\n    if (captcha?.verify) {\n      reqParams.captcha_code = captcha.captcha_code;\n      reqParams.captcha_id = captcha.captcha_id;\n    }\n\n    register(reqParams)\n      .then(async (res) => {\n        await emailCaptcha?.close();\n        updateUser(res);\n        callback();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          emailCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = async (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    if (!emailCaptcha) {\n      handleRegister();\n      return;\n    }\n    emailCaptcha.check(() => {\n      handleRegister();\n    });\n  };\n\n  return (\n    <>\n      <Form noValidate onSubmit={handleSubmit} autoComplete=\"off\">\n        <Form.Group controlId=\"name\" className=\"mb-3\">\n          <Form.Label>{t('name.label')}</Form.Label>\n          <Form.Control\n            autoComplete=\"off\"\n            required\n            type=\"text\"\n            isInvalid={formData.name.isInvalid}\n            value={formData.name.value}\n            onChange={(e) =>\n              handleChange({\n                name: {\n                  value: e.target.value,\n                  isInvalid: false,\n                  errorMsg: '',\n                },\n              })\n            }\n          />\n          <Form.Control.Feedback type=\"invalid\">\n            {formData.name.errorMsg}\n          </Form.Control.Feedback>\n        </Form.Group>\n        <Form.Group controlId=\"email\" className=\"mb-3\">\n          <Form.Label>{t('email.label')}</Form.Label>\n          <Form.Control\n            autoComplete=\"off\"\n            required\n            type=\"e_mail\"\n            isInvalid={formData.e_mail.isInvalid}\n            value={formData.e_mail.value}\n            onChange={(e) =>\n              handleChange({\n                e_mail: {\n                  value: e.target.value,\n                  isInvalid: false,\n                  errorMsg: '',\n                },\n              })\n            }\n          />\n          <Form.Control.Feedback type=\"invalid\">\n            {formData.e_mail.errorMsg}\n          </Form.Control.Feedback>\n        </Form.Group>\n\n        <Form.Group controlId=\"password\" className=\"mb-3\">\n          <Form.Label>{t('password.label')}</Form.Label>\n          <Form.Control\n            autoComplete=\"off\"\n            required\n            type=\"password\"\n            isInvalid={formData.pass.isInvalid}\n            value={formData.pass.value}\n            onChange={(e) =>\n              handleChange({\n                pass: {\n                  value: e.target.value,\n                  isInvalid: false,\n                  errorMsg: '',\n                },\n              })\n            }\n          />\n          <Form.Control.Feedback type=\"invalid\">\n            {formData.pass.errorMsg}\n          </Form.Control.Feedback>\n        </Form.Group>\n\n        <div className=\"d-grid\">\n          <Button variant=\"primary\" type=\"submit\">\n            {t('signup', { keyPrefix: 'btns' })}\n          </Button>\n        </div>\n      </Form>\n      <div className=\"text-center small mt-3\">\n        <Trans i18nKey=\"login.agreements\" ns=\"translation\">\n          By registering, you agree to the\n          <Link\n            to=\"/privacy\"\n            onClick={(evt) => {\n              legalClick(evt, 'privacy');\n            }}\n            target=\"_blank\">\n            privacy policy\n          </Link>\n          and\n          <Link\n            to=\"/tos\"\n            onClick={(evt) => {\n              legalClick(evt, 'tos');\n            }}\n            target=\"_blank\">\n            terms of service\n          </Link>\n          .\n        </Trans>\n      </div>\n    </>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Register/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState } from 'react';\nimport { Container, Col } from 'react-bootstrap';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\nimport { Unactivate, WelcomeTitle, PluginRender } from '@/components';\nimport { guard } from '@/utils';\nimport { loginSettingStore } from '@/stores';\nimport { PluginType } from '@/utils/pluginKit/interface';\n\nimport SignUpForm from './components/SignUpForm';\n\nconst Index: React.FC = () => {\n  const [showForm, setShowForm] = useState(true);\n  const { t } = useTranslation('translation', { keyPrefix: 'login' });\n  const loginSetting = loginSettingStore((state) => state.login);\n  const onStep = () => {\n    setShowForm((bol) => !bol);\n  };\n  usePageTags({\n    title: t('sign_up', { keyPrefix: 'page_title' }),\n  });\n\n  if (!guard.singUpAgent().ok) {\n    return null;\n  }\n\n  const showSignupForm =\n    loginSetting?.allow_new_registrations &&\n    loginSetting.allow_email_registrations;\n\n  return (\n    <Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>\n      <WelcomeTitle />\n\n      {showForm ? (\n        <Col className=\"mx-auto\" md={6} lg={4} xl={3}>\n          <PluginRender\n            type={PluginType.Connector}\n            slug_name=\"third_party_connector\"\n            className=\"mb-5\"\n          />\n          {showSignupForm ? <SignUpForm callback={onStep} /> : null}\n          <div className=\"text-center mt-5\">\n            <Trans i18nKey=\"login.info_login\" ns=\"translation\">\n              Already have an account? <Link to=\"/users/login\">Log in</Link>\n            </Trans>\n          </div>\n        </Col>\n      ) : (\n        <Unactivate visible={!showForm} />\n      )}\n    </Container>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, FormEvent, useEffect, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport type * as Type from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport { getLoggedUserInfo, changeEmail } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.account',\n  });\n  const [step, setStep] = useState(1);\n  const [formData, setFormData] = useState<Type.FormDataType>({\n    e_mail: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n  const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();\n  const toast = useToast();\n  const emailCaptcha = useCaptchaPlugin('edit_userinfo');\n\n  useEffect(() => {\n    getLoggedUserInfo().then((resp) => {\n      setUserInfo(resp);\n    });\n  }, []);\n\n  const handleChange = (params: Type.FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { e_mail, pass } = formData;\n\n    if (!e_mail.value) {\n      bol = false;\n      formData.e_mail = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('new_email.msg'),\n      };\n    }\n\n    if (!pass.value) {\n      bol = false;\n      formData.pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('pass.msg'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n    return bol;\n  };\n\n  const initFormData = () => {\n    setFormData({\n      e_mail: {\n        value: '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n      pass: {\n        value: '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const postEmail = (event?: any) => {\n    if (event) {\n      event.preventDefault();\n    }\n    const params: any = {\n      e_mail: formData.e_mail.value,\n      pass: formData.pass.value,\n    };\n\n    const imgCode = emailCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      params.captcha_code = imgCode.captcha_code;\n      params.captcha_id = imgCode.captcha_id;\n    }\n    changeEmail(params)\n      .then(async () => {\n        await emailCaptcha?.close();\n        setStep(1);\n        toast.onShow({\n          msg: t('change_email_info'),\n          variant: 'warning',\n        });\n        initFormData();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          emailCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    if (!emailCaptcha) {\n      postEmail();\n      return;\n    }\n    emailCaptcha.check(() => {\n      postEmail();\n    });\n  };\n\n  return (\n    <div>\n      {step === 1 && (\n        <Form>\n          <Form.Group controlId=\"oldEmail\" className=\"mb-3\">\n            <Form.Label>{t('email.label')}</Form.Label>\n            <Form.Control\n              type=\"text\"\n              disabled\n              defaultValue={userInfo?.e_mail?.replace(\n                /(.{2})(.+)(@.+)/i,\n                '$1****$3',\n              )}\n            />\n          </Form.Group>\n\n          <Button\n            variant=\"outline-secondary\"\n            onClick={() => {\n              setStep(2);\n            }}>\n            {t('change_email_btn')}\n          </Button>\n        </Form>\n      )}\n      {step === 2 && (\n        <Form noValidate onSubmit={handleSubmit}>\n          <Form.Group controlId=\"pass\" className=\"mb-3\">\n            <Form.Label>{t('pass.label')}</Form.Label>\n            <Form.Control\n              autoComplete=\"new-password\"\n              required\n              type=\"password\"\n              isInvalid={formData.pass.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  pass: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.pass.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"e_mail\" className=\"mb-3\">\n            <Form.Label>{t('new_email.label')}</Form.Label>\n            <Form.Control\n              autoComplete=\"off\"\n              required\n              type=\"email\"\n              placeholder=\"\"\n              value={formData.e_mail.value}\n              isInvalid={formData.e_mail.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  e_mail: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.e_mail.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <div>\n            <Button type=\"submit\" variant=\"primary\" className=\"me-2\">\n              {t('save', { keyPrefix: 'btns' })}\n            </Button>\n\n            <Button variant=\"link\" onClick={() => setStep(1)}>\n              {t('cancel', { keyPrefix: 'btns' })}\n            </Button>\n          </div>\n        </Form>\n      )}\n    </div>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC, FormEvent, useState } from 'react';\nimport { Form, Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classname from 'classnames';\n\nimport { useToast } from '@/hooks';\nimport { useCaptchaPlugin } from '@/utils/pluginKit';\nimport type { FormDataType } from '@/common/interface';\nimport { modifyPassword } from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\nimport { loggedUserInfoStore } from '@/stores';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.account',\n  });\n  const { user } = loggedUserInfoStore();\n  const [showForm, setFormState] = useState(false);\n  const toast = useToast();\n  const [formData, setFormData] = useState<FormDataType>({\n    old_pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    pass: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    pass2: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const infoCaptcha = useCaptchaPlugin('edit_userinfo');\n\n  const handleFormState = () => {\n    setFormState((pre) => !pre);\n  };\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { old_pass, pass, pass2 } = formData;\n    if (!old_pass.value && user.have_password) {\n      bol = false;\n      formData.old_pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.empty'),\n      };\n    }\n\n    if (!pass.value) {\n      bol = false;\n      formData.pass = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.empty'),\n      };\n    }\n\n    if (bol && pass.value && pass.value.length < 8) {\n      bol = false;\n      formData.pass = {\n        value: pass.value,\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.length'),\n      };\n    }\n\n    if (!pass2.value) {\n      bol = false;\n      formData.pass2 = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.empty'),\n      };\n    }\n\n    if (bol && pass2.value && pass2.value.length < 8) {\n      bol = false;\n      formData.pass2 = {\n        value: pass2.value,\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.length'),\n      };\n    }\n    if (bol && pass.value && pass2.value && pass.value !== pass2.value) {\n      bol = false;\n      formData.pass2 = {\n        value: pass2.value,\n        isInvalid: true,\n        errorMsg: t('current_pass.msg.different'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n\n    return bol;\n  };\n\n  const postModifyPass = (event?: any) => {\n    if (event) {\n      event.preventDefault();\n    }\n    const params: any = {\n      old_pass: formData.old_pass.value,\n      pass: formData.pass.value,\n    };\n\n    const imgCode = infoCaptcha?.getCaptcha();\n    if (imgCode?.verify) {\n      params.captcha_code = imgCode.captcha_code;\n      params.captcha_id = imgCode.captcha_id;\n    }\n    modifyPassword(params)\n      .then(async () => {\n        await infoCaptcha?.close();\n        toast.onShow({\n          msg: t('update_password', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n        handleFormState();\n      })\n      .catch((err) => {\n        if (err.isError) {\n          infoCaptcha?.handleCaptchaError(err.list);\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n    if (!infoCaptcha) {\n      postModifyPass();\n      return;\n    }\n\n    infoCaptcha.check(() => {\n      postModifyPass();\n    });\n  };\n\n  return (\n    <div className=\"mt-5\">\n      {showForm ? (\n        <Form noValidate onSubmit={handleSubmit}>\n          <Form.Group\n            controlId=\"old_pass\"\n            className={classname('mb-3', user.have_password ? '' : 'd-none')}>\n            <Form.Label>{t('current_pass.label')}</Form.Label>\n            <Form.Control\n              autoComplete=\"off\"\n              required\n              type=\"password\"\n              placeholder=\"\"\n              isInvalid={formData.old_pass.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  old_pass: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.old_pass.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"new_pass\" className=\"mb-3\">\n            <Form.Label>{t('new_pass.label')}</Form.Label>\n            <Form.Control\n              autoComplete=\"off\"\n              required\n              type=\"password\"\n              isInvalid={formData.pass.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  pass: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.pass.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"pass2\" className=\"mb-3\">\n            <Form.Label>{t('pass_confirm.label')}</Form.Label>\n            <Form.Control\n              autoComplete=\"off\"\n              required\n              type=\"password\"\n              isInvalid={formData.pass2.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  pass2: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.pass2.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n          <div>\n            <Button type=\"submit\" variant=\"primary\" className=\"me-2\">\n              {t('save', { keyPrefix: 'btns' })}\n            </Button>\n\n            <Button variant=\"link\" onClick={() => handleFormState()}>\n              {t('cancel', { keyPrefix: 'btns' })}\n            </Button>\n          </div>\n        </Form>\n      ) : (\n        <>\n          <Form.Label>{t('password_title')}</Form.Label>\n          <br />\n          <Button\n            variant=\"outline-secondary\"\n            type=\"submit\"\n            onClick={() => {\n              handleFormState();\n            }}>\n            {t('change_pass_btn')}\n          </Button>\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Account/components/MyLogins/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport { Modal } from '@/components';\nimport { useOauthConnectorInfoByUser, userOauthUnbind } from '@/services';\nimport { useToast } from '@/hooks';\nimport { base64ToSvg } from '@/utils';\nimport Storage from '@/utils/storage';\nimport { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';\nimport { REACT_BASE_PATH } from '@/router/alias';\n\nconst Index = () => {\n  const { data, mutate } = useOauthConnectorInfoByUser();\n  const toast = useToast();\n\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.my_logins',\n  });\n\n  const { t: t2 } = useTranslation('translation', {\n    keyPrefix: 'oauth',\n  });\n\n  const deleteLogins = (e, item) => {\n    if (!item.binding) {\n      Storage.set(\n        REDIRECT_PATH_STORAGE_KEY,\n        window.location.pathname.replace(REACT_BASE_PATH, ''),\n      );\n      return;\n    }\n    e.preventDefault();\n    Modal.confirm({\n      title: t('modal_title'),\n      content: t('modal_content'),\n      confirmBtnVariant: 'danger',\n      confirmText: t('modal_confirm_btn'),\n      onConfirm: () => {\n        userOauthUnbind({ external_id: item.external_id }).then(() => {\n          mutate();\n          toast.onShow({\n            msg: t('remove_success'),\n            variant: 'success',\n          });\n        });\n      },\n    });\n  };\n\n  if (!data?.length) return null;\n  return (\n    <div className=\"mt-5\">\n      <div className=\"form-label\">{t('title')}</div>\n      <small className=\"form-text mt-0\">{t('label')}</small>\n\n      <div className=\"d-grid gap-2 mt-3\">\n        {data?.map((item) => {\n          return (\n            <div key={item.name}>\n              <Button\n                variant={item.binding ? 'outline-danger' : 'outline-secondary'}\n                href={item.link}\n                onClick={(e) => deleteLogins(e, item)}>\n                <span\n                  dangerouslySetInnerHTML={{\n                    __html: base64ToSvg(item.icon, 'btnSvg me-2'),\n                  }}\n                />\n                <span>\n                  {t2(item.binding ? 'remove' : 'connect', {\n                    auth_name: item.name,\n                  })}\n                </span>\n              </Button>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Account/components/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport ModifyEmail from './ModifyEmail';\nimport ModifyPassword from './ModifyPass';\nimport MyLogins from './MyLogins';\n\nexport { ModifyEmail, ModifyPassword, MyLogins };\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Account/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { userCenterStore } from '@/stores';\nimport { getUcSettings, UcSettingAgent } from '@/services';\n\nimport { ModifyEmail, ModifyPassword, MyLogins } from './components';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.account',\n  });\n  const { agent: ucAgent } = userCenterStore();\n  const [accountAgent, setAccountAgent] = useState<UcSettingAgent>();\n\n  const initData = () => {\n    if (ucAgent?.enabled) {\n      getUcSettings().then((resp) => {\n        setAccountAgent(resp.account_setting_agent);\n      });\n    }\n  };\n  useEffect(() => {\n    initData();\n  }, []);\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('heading')}</h3>\n      {accountAgent?.enabled && accountAgent?.redirect_url ? (\n        <a href={accountAgent.redirect_url}>\n          {t('goto_modify', { keyPrefix: 'settings' })}\n        </a>\n      ) : null}\n      {!ucAgent?.enabled || accountAgent?.enabled === false ? (\n        <>\n          <ModifyEmail />\n          <ModifyPassword />\n          <MyLogins />\n        </>\n      ) : null}\n    </>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Interface/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useEffect, useState, FormEvent } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type { LangsType, FormDataType } from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { updateUserInterface } from '@/services';\nimport { localize } from '@/utils';\nimport { loggedUserInfoStore } from '@/stores';\nimport { SchemaForm, JSONSchema, UISchema } from '@/components';\n\nconst Index = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.interface',\n  });\n  const loggedUserInfo = loggedUserInfoStore.getState().user;\n  const toast = useToast();\n  const [langs, setLangs] = useState<LangsType[]>();\n  const [formData, setFormData] = useState<FormDataType>({\n    language: {\n      value: loggedUserInfo.language,\n      isInvalid: false,\n      errorMsg: '',\n    },\n    color_scheme: {\n      value: loggedUserInfo.color_scheme || 'default',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n  const schema: JSONSchema = {\n    title: t('heading'),\n    properties: {\n      language: {\n        type: 'string',\n        title: t('lang.label'),\n        description: t('lang.text'),\n        enum: langs?.map((_) => _.value),\n        enumNames: langs?.map((_) => _.label),\n        default: loggedUserInfo.language,\n      },\n      color_scheme: {\n        type: 'string',\n        title: t('color_scheme.label', { keyPrefix: 'admin.themes' }),\n        enum: ['default', 'system', 'light', 'dark'],\n        enumNames: [\n          t('default', { keyPrefix: 'btns' }),\n          t('system_setting', { keyPrefix: 'btns' }),\n          t('light', { keyPrefix: 'btns' }),\n          t('dark', { keyPrefix: 'btns' }),\n        ],\n        default: loggedUserInfo.color_scheme,\n      },\n    },\n  };\n\n  const uiSchema: UISchema = {\n    language: {\n      'ui:widget': 'select',\n    },\n    color_scheme: {\n      'ui:widget': 'select',\n    },\n  };\n\n  const getLangs = async () => {\n    const res: LangsType[] = await localize.loadLanguageOptions();\n    setFormData({\n      ...formData,\n      language: {\n        ...formData.language,\n        value: loggedUserInfo.language || res[0].value,\n      },\n    });\n    setLangs(res);\n  };\n\n  const handleOnChange = (d) => {\n    setFormData(d);\n  };\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    const params = {\n      language: formData.language.value,\n      color_scheme: formData.color_scheme.value,\n    };\n    updateUserInterface(params).then(() => {\n      loggedUserInfoStore.getState().update({\n        ...loggedUserInfo,\n        ...params,\n      });\n      localize.setupAppLanguage();\n      localize.setupAppTheme();\n      toast.onShow({\n        msg: t('update', { keyPrefix: 'toast' }),\n        variant: 'success',\n      });\n    });\n  };\n\n  useEffect(() => {\n    getLangs();\n  }, []);\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('heading')}</h3>\n      <SchemaForm\n        schema={schema}\n        uiSchema={uiSchema}\n        formData={formData}\n        onChange={handleOnChange}\n        onSubmit={handleSubmit}\n      />\n    </>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Notification/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { useState, FormEvent, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport type { FormDataType, NotificationConfig } from '@/common/interface';\nimport { useToast } from '@/hooks';\nimport { useGetNotificationConfig, putNotificationConfig } from '@/services';\nimport { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';\n\nconst Index = () => {\n  const toast = useToast();\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.notification',\n  });\n  const { data: configData } = useGetNotificationConfig();\n\n  const schema: JSONSchema = {\n    title: t('heading'),\n    properties: {\n      inbox: {\n        type: 'boolean',\n        title: t('inbox.label'),\n        description: t('inbox.description'),\n        default: configData?.inbox.enable,\n      },\n      all_new_question: {\n        type: 'boolean',\n        title: t('all_new_question.label'),\n        description: t('all_new_question.description'),\n        default: configData?.all_new_question.enable,\n      },\n      all_new_question_for_following_tags: {\n        type: 'boolean',\n        title: t('all_new_question_for_following_tags.label'),\n        description: t('all_new_question_for_following_tags.description'),\n        default: configData?.all_new_question_for_following_tags.enable,\n      },\n    },\n  };\n  const uiSchema: UISchema = {\n    inbox: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('turn_on'),\n      },\n    },\n    all_new_question: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('turn_on'),\n      },\n    },\n    all_new_question_for_following_tags: {\n      'ui:widget': 'switch',\n      'ui:options': {\n        label: t('turn_on'),\n        text: t('all_new_question_for_following_tags.description'),\n      },\n    },\n  };\n  const [formData, setFormData] = useState<FormDataType>(initFormData(schema));\n\n  useEffect(() => {\n    setFormData(initFormData(schema));\n  }, [configData]);\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    const params = {\n      inbox: {\n        enable: formData.inbox.value,\n        key: configData?.inbox.key,\n      },\n      all_new_question: {\n        enable: formData.all_new_question.value,\n        key: configData?.all_new_question.key,\n      },\n      all_new_question_for_following_tags: {\n        enable: formData.all_new_question_for_following_tags.value,\n        key: configData?.all_new_question_for_following_tags.key,\n      },\n    } as NotificationConfig;\n\n    putNotificationConfig(params).then(() => {\n      toast.onShow({\n        msg: t('update', { keyPrefix: 'toast' }),\n        variant: 'success',\n      });\n    });\n  };\n\n  const handleChange = (ud) => {\n    setFormData(ud);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('heading')}</h3>\n      <SchemaForm\n        schema={schema}\n        uiSchema={uiSchema}\n        formData={formData}\n        onChange={handleChange}\n        onSubmit={handleSubmit}\n      />\n    </>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Plugins/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useParams } from 'react-router-dom';\n\nimport { useToast } from '@/hooks';\nimport type * as Types from '@/common/interface';\nimport { SchemaForm, JSONSchema, UISchema } from '@/components';\nimport { useGetUserPluginConfig, updateUserPluginConfig } from '@/services';\nimport {\n  InputOptions,\n  FormKit,\n  initFormData,\n  mergeFormData,\n} from '@/components/SchemaForm';\n\nconst Config = () => {\n  const { t } = useTranslation('translation');\n  const { slug_name } = useParams<{ slug_name: string }>();\n  const { data, mutate: refreshPluginConfig } = useGetUserPluginConfig({\n    plugin_slug_name: slug_name,\n  });\n  const Toast = useToast();\n  const [schema, setSchema] = useState<JSONSchema | null>(null);\n  const [uiSchema, setUISchema] = useState<UISchema>();\n  const required: string[] = [];\n\n  const [formData, setFormData] = useState<Types.FormDataType | null>(null);\n\n  useEffect(() => {\n    if (!data) {\n      return;\n    }\n    const properties: JSONSchema['properties'] = {};\n    const uiConf: UISchema = {};\n    data.config_fields?.forEach((item) => {\n      properties[item.name] = {\n        type: 'string',\n        title: item.title,\n        description: item.description,\n        default: item.value,\n      };\n\n      if (item.options instanceof Array) {\n        properties[item.name].enum = item.options.map((option) => option.value);\n        properties[item.name].enumNames = item.options.map(\n          (option) => option.label,\n        );\n      }\n      uiConf[item.name] = {};\n      uiConf[item.name]['ui:widget'] = item.type;\n      if (item.ui_options) {\n        if ((item.ui_options as InputOptions & { input_type })?.input_type) {\n          (item.ui_options as InputOptions).inputType = (\n            item.ui_options as InputOptions & { input_type }\n          ).input_type;\n        }\n        uiConf[item.name]['ui:options'] = item.ui_options;\n      }\n      if (item.required) {\n        required.push(item.name);\n      }\n    });\n    const result = {\n      title: data?.name || '',\n      required,\n      properties,\n    };\n    setSchema(result);\n    setUISchema(uiConf);\n    setFormData(mergeFormData(formData, initFormData(result)));\n  }, [data?.config_fields]);\n\n  const onSubmit = (evt) => {\n    if (!formData) {\n      return;\n    }\n    evt.preventDefault();\n    evt.stopPropagation();\n    const config_fields = {};\n    Object.keys(formData).forEach((key) => {\n      config_fields[key] = formData[key].value;\n    });\n    const params = {\n      plugin_slug_name: slug_name,\n      config_fields,\n    };\n    updateUserPluginConfig(params).then(() => {\n      Toast.onShow({\n        msg: t('update', { keyPrefix: 'toast' }),\n        variant: 'success',\n      });\n    });\n  };\n  const refreshConfig: FormKit['refreshConfig'] = async () => {\n    refreshPluginConfig();\n  };\n  const handleOnChange = (form) => {\n    setFormData(form);\n  };\n  return (\n    <>\n      <h3 className=\"mb-4\">{data?.name}</h3>\n      <SchemaForm\n        schema={schema}\n        uiSchema={uiSchema}\n        refreshConfig={refreshConfig}\n        formData={formData}\n        onSubmit={onSubmit}\n        onChange={handleOnChange}\n      />\n    </>\n  );\n};\n\nexport default Config;\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/Profile/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FormEvent, useState, useEffect } from 'react';\nimport { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { sha256 } from 'js-sha256';\n\nimport type { FormDataType } from '@/common/interface';\nimport { UploadImg, Avatar, Icon, ImgViewer } from '@/components';\nimport { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores';\nimport { useToast } from '@/hooks';\nimport {\n  modifyUserInfo,\n  getLoggedUserInfo,\n  getUcSettings,\n  UcSettingAgent,\n} from '@/services';\nimport { handleFormError, scrollToElementTop } from '@/utils';\n\nconst Index: React.FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.profile',\n  });\n  const toast = useToast();\n  const { user, update } = loggedUserInfoStore();\n  const { agent: ucAgent } = userCenterStore();\n  const { users: usersSettings } = siteInfoStore();\n  const [mailHash, setMailHash] = useState('');\n  const [count] = useState(0);\n  const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();\n  const [formData, setFormData] = useState<FormDataType>({\n    display_name: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    username: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    avatar: {\n      type: 'default',\n      gravatar: '',\n      custom: '',\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    bio: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    website: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n    location: {\n      value: '',\n      isInvalid: false,\n      errorMsg: '',\n    },\n  });\n\n  const handleChange = (params: FormDataType) => {\n    setFormData({ ...formData, ...params });\n  };\n  const handleAvatarChange = (evt) => {\n    const { value: v } = evt.currentTarget;\n    if (v === 'gravatar') {\n      handleChange({\n        avatar: {\n          ...formData.avatar,\n          type: 'gravatar',\n          gravatar: `https://www.gravatar.com/avatar/${mailHash}`,\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n    }\n    if (v === 'custom') {\n      handleChange({\n        avatar: {\n          ...formData.avatar,\n          type: 'custom',\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n    }\n    if (v === 'default') {\n      handleChange({\n        avatar: {\n          ...formData.avatar,\n          type: 'default',\n          isInvalid: false,\n          errorMsg: '',\n        },\n      });\n    }\n  };\n\n  const avatarUpload = (path: string) => {\n    setFormData({\n      ...formData,\n      avatar: {\n        ...formData.avatar,\n        type: 'custom',\n        custom: path,\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n  const removeCustomAvatar = () => {\n    setFormData({\n      ...formData,\n      avatar: {\n        ...formData.avatar,\n        custom: '',\n        isInvalid: false,\n        errorMsg: '',\n      },\n    });\n  };\n\n  const checkValidated = (): boolean => {\n    let bol = true;\n    const { display_name, website, username } = formData;\n    if (!display_name.value) {\n      bol = false;\n      formData.display_name = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('display_name.msg'),\n      };\n    } else if ([...display_name.value].length > 30) {\n      bol = false;\n      formData.display_name = {\n        value: display_name.value,\n        isInvalid: true,\n        errorMsg: t('display_name.msg_range'),\n      };\n    }\n\n    if (!username.value) {\n      bol = false;\n      formData.username = {\n        value: '',\n        isInvalid: true,\n        errorMsg: t('username.msg'),\n      };\n    } else if ([...username.value].length > 30) {\n      bol = false;\n      formData.username = {\n        value: username.value,\n        isInvalid: true,\n        errorMsg: t('username.msg_range'),\n      };\n    } else if (/[^a-z0-9\\-._]/.test(username.value)) {\n      bol = false;\n      formData.username = {\n        value: username.value,\n        isInvalid: true,\n        errorMsg: t('username.character'),\n      };\n    }\n\n    if (formData.avatar.type === 'custom' && !formData.avatar.custom) {\n      bol = false;\n      formData.avatar = {\n        ...formData.avatar,\n        custom: '',\n        value: '',\n        isInvalid: true,\n        errorMsg: t('avatar.msg'),\n      };\n    }\n\n    const reg = /^(http|https):\\/\\//g;\n    if (website.value && !website.value.match(reg)) {\n      bol = false;\n      formData.website = {\n        value: formData.website.value,\n        isInvalid: true,\n        errorMsg: t('website.msg'),\n      };\n    }\n    setFormData({\n      ...formData,\n    });\n    if (!bol) {\n      const errObj = Object.keys(formData).filter(\n        (key) => formData[key].isInvalid,\n      );\n      const ele = document.getElementById(errObj[0]);\n      scrollToElementTop(ele);\n    }\n    return bol;\n  };\n\n  const handleSubmit = (event: FormEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    if (!checkValidated()) {\n      return;\n    }\n\n    const params = {\n      display_name: formData.display_name.value,\n      username: formData.username.value,\n      avatar: {\n        type: formData.avatar.type,\n        gravatar: formData.avatar.gravatar,\n        custom: formData.avatar.custom,\n      },\n      bio: formData.bio.value,\n      website: formData.website.value,\n      location: formData.location.value,\n    };\n\n    modifyUserInfo(params)\n      .then(() => {\n        update({\n          ...user,\n          ...params,\n        });\n        toast.onShow({\n          msg: t('update', { keyPrefix: 'toast' }),\n          variant: 'success',\n        });\n      })\n      .catch((err) => {\n        if (err.isError) {\n          const data = handleFormError(err, formData);\n          setFormData({ ...data });\n          const ele = document.getElementById(err.list[0].error_field);\n          scrollToElementTop(ele);\n        }\n      });\n  };\n\n  const getProfile = () => {\n    getLoggedUserInfo().then((res) => {\n      formData.display_name.value = res.display_name;\n      formData.username.value = res.username;\n      formData.bio.value = res.bio;\n      formData.avatar.type = res.avatar.type || 'default';\n      formData.avatar.gravatar = res.avatar.gravatar;\n      formData.avatar.custom = res.avatar.custom;\n      formData.location.value = res.location;\n      formData.website.value = res.website;\n      setFormData({ ...formData });\n      if (res.e_mail) {\n        const str = res.e_mail.toLowerCase().trim();\n        const hash = sha256(str);\n        setMailHash(hash);\n      }\n    });\n  };\n  const initData = () => {\n    if (ucAgent?.enabled) {\n      getUcSettings().then((resp) => {\n        setProfileAgent(resp.profile_setting_agent);\n        if (resp.profile_setting_agent?.enabled === false) {\n          getProfile();\n        }\n      });\n    } else {\n      getProfile();\n    }\n  };\n  useEffect(() => {\n    initData();\n  }, []);\n\n  return (\n    <>\n      <h3 className=\"mb-4\">{t('heading')}</h3>\n      {profileAgent?.enabled && profileAgent?.redirect_url ? (\n        <a href={profileAgent.redirect_url}>\n          {t('goto_modify', { keyPrefix: 'settings' })}\n        </a>\n      ) : null}\n      {!ucAgent?.enabled || profileAgent?.enabled === false ? (\n        <Form noValidate onSubmit={handleSubmit}>\n          <Form.Group controlId=\"display_name\" className=\"mb-3\">\n            <Form.Label>{t('display_name.label')}</Form.Label>\n            <Form.Control\n              required\n              type=\"text\"\n              value={formData.display_name.value}\n              isInvalid={formData.display_name.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  display_name: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.display_name.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"username\" className=\"mb-3\">\n            <Form.Label>{t('username.label')}</Form.Label>\n            <Form.Control\n              required\n              type=\"text\"\n              value={formData.username.value}\n              isInvalid={formData.username.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  username: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Text as=\"div\">{t('username.caption')}</Form.Text>\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.username.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"avatar\" className=\"mb-3\">\n            <Form.Label>{t('avatar.label')}</Form.Label>\n            <div className=\"mb-3\">\n              <Form.Select\n                name=\"avatar.type\"\n                value={formData.avatar.type}\n                onChange={handleAvatarChange}>\n                <option value=\"gravatar\" key=\"gravatar\">\n                  {t('avatar.gravatar')}\n                </option>\n                <option value=\"default\" key=\"default\">\n                  {t('avatar.default')}\n                </option>\n                <option value=\"custom\" key=\"custom\">\n                  {t('avatar.custom')}\n                </option>\n              </Form.Select>\n            </div>\n            <ImgViewer>\n              <div className=\"d-flex\">\n                {formData.avatar.type === 'gravatar' && (\n                  <Stack>\n                    <Avatar\n                      size=\"160px\"\n                      avatar={formData.avatar.gravatar}\n                      searchStr={`s=256&d=identicon${\n                        count > 0 ? `&t=${new Date().valueOf()}` : ''\n                      }`}\n                      className=\"me-3 rounded\"\n                      alt={formData.display_name.value}\n                    />\n                    <Form.Text className=\"mt-1\">\n                      <span>{t('avatar.gravatar_text')}</span>\n                      <a\n                        href={\n                          usersSettings.gravatar_base_url.includes(\n                            'gravatar.cn',\n                          )\n                            ? 'https://gravatar.cn'\n                            : 'https://gravatar.com'\n                        }\n                        className=\"ms-1\"\n                        target=\"_blank\"\n                        rel=\"noreferrer\">\n                        {usersSettings.gravatar_base_url.includes('gravatar.cn')\n                          ? 'gravatar.cn'\n                          : 'gravatar.com'}\n                      </a>\n                    </Form.Text>\n                  </Stack>\n                )}\n\n                {formData.avatar.type === 'custom' && (\n                  <Stack>\n                    <Stack direction=\"horizontal\" className=\"align-items-start\">\n                      <Avatar\n                        size=\"160px\"\n                        searchStr=\"s=256\"\n                        avatar={formData.avatar.custom}\n                        className=\"me-2 bg-gray-300 \"\n                        alt={formData.display_name.value}\n                      />\n                      <ButtonGroup vertical className=\"fit-content\">\n                        <UploadImg type=\"avatar\" uploadCallback={avatarUpload}>\n                          <Icon name=\"cloud-upload\" />\n                        </UploadImg>\n                        <Button\n                          variant=\"outline-secondary\"\n                          onClick={removeCustomAvatar}>\n                          <Icon name=\"trash\" />\n                        </Button>\n                      </ButtonGroup>\n                    </Stack>\n                    <Form.Text className=\"mt-1\">\n                      <Trans i18nKey=\"settings.profile.avatar.text\">\n                        You can upload your image.\n                      </Trans>\n                    </Form.Text>\n                  </Stack>\n                )}\n                {formData.avatar.type === 'default' && (\n                  <Avatar\n                    size=\"160px\"\n                    avatar=\"\"\n                    alt={formData.display_name.value}\n                  />\n                )}\n              </div>\n            </ImgViewer>\n            <Form.Control\n              isInvalid={formData.avatar.isInvalid}\n              className=\"d-none\"\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.avatar.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"bio\" className=\"mb-3\">\n            <Form.Label>\n              {`${t('bio.label')} ${t('optional', {\n                keyPrefix: 'form',\n              })}`}\n            </Form.Label>\n            <Form.Control\n              className=\"font-monospace\"\n              required\n              as=\"textarea\"\n              rows={5}\n              value={formData.bio.value}\n              isInvalid={formData.bio.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  bio: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.bio.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"website\" className=\"mb-3\">\n            <Form.Label>{`${t('website.label')} ${t('optional', {\n              keyPrefix: 'form',\n            })}`}</Form.Label>\n            <Form.Control\n              required\n              type=\"url\"\n              placeholder={t('website.placeholder')}\n              value={formData.website.value}\n              isInvalid={formData.website.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  website: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.website.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Form.Group controlId=\"location\" className=\"mb-3\">\n            <Form.Label>{`${t('location.label')} ${t('optional', {\n              keyPrefix: 'form',\n            })}`}</Form.Label>\n            <Form.Control\n              required\n              type=\"text\"\n              placeholder={t('location.placeholder')}\n              value={formData.location.value}\n              isInvalid={formData.location.isInvalid}\n              onChange={(e) =>\n                handleChange({\n                  location: {\n                    value: e.target.value,\n                    isInvalid: false,\n                    errorMsg: '',\n                  },\n                })\n              }\n            />\n            <Form.Control.Feedback type=\"invalid\">\n              {formData.location.errorMsg}\n            </Form.Control.Feedback>\n          </Form.Group>\n\n          <Button variant=\"primary\" type=\"submit\">\n            {t('btn_name')}\n          </Button>\n        </Form>\n      ) : null}\n    </>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/components/Nav/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport React, { FC } from 'react';\nimport { Nav } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { NavLink, useMatch } from 'react-router-dom';\n\nimport { useGetUserPluginList } from '@/services';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'settings.nav' });\n  const settingMatch = useMatch('/users/settings/:setting');\n  const { data } = useGetUserPluginList();\n\n  return (\n    <Nav variant=\"pills\" className=\"flex-column\">\n      <NavLink\n        className={({ isActive }) =>\n          isActive || !settingMatch ? 'nav-link active' : 'nav-link'\n        }\n        to=\"/users/settings/profile\">\n        {t('profile')}\n      </NavLink>\n      <NavLink className=\"nav-link\" to=\"/users/settings/notify\">\n        {t('notification')}\n      </NavLink>\n      <NavLink className=\"nav-link\" to=\"/users/settings/account\">\n        {t('account')}\n      </NavLink>\n      <NavLink className=\"nav-link\" to=\"/users/settings/interface\">\n        {t('interface')}\n      </NavLink>\n      {data?.map((item) => {\n        return (\n          <NavLink\n            key={item.slug_name}\n            className=\"nav-link w-100 text-truncate\"\n            to={`/users/settings/${item.slug_name}`}>\n            {item.name}\n          </NavLink>\n        );\n      })}\n    </Nav>\n  );\n};\n\nexport default React.memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/index.scss",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n.settings-nav {\n  flex: none;\n  width: 20%;\n}\n\n.settings-main {\n  flex: none;\n  width: 60%;\n}\n\n// lg\n@media screen and (max-width: 1199.9px) {\n  .settings-nav {\n    width: 30%;\n  }\n  .settings-main {\n    width: 70%;\n  }\n}\n\n// sm\n@media screen and (max-width: 767.9px) {\n  .settings-nav {\n    width: 100%;\n  }\n  .settings-main {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "ui/src/pages/Users/Settings/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo } from 'react';\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Outlet } from 'react-router-dom';\n\nimport { usePageTags } from '@/hooks';\n\nimport Nav from './components/Nav';\n\nimport './index.scss';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', {\n    keyPrefix: 'settings.profile',\n  });\n\n  usePageTags({\n    title: t('settings', { keyPrefix: 'page_title' }),\n  });\n  return (\n    <Row className=\"mt-4 mb-5 pb-5\">\n      <Col className=\"settings-nav mb-4\">\n        <Nav />\n      </Col>\n      <Col className=\"settings-main\">\n        <Outlet />\n      </Col>\n    </Row>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/Suspended/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { useTranslation } from 'react-i18next';\nimport { Button } from 'react-bootstrap';\n\nimport { siteInfoStore } from '@/stores';\nimport { usePageTags } from '@/hooks';\n\nconst Suspended = () => {\n  const { contact_email = '' } = siteInfoStore((state) => state.siteInfo);\n  const { t } = useTranslation('translation', { keyPrefix: 'suspended' });\n  usePageTags({\n    title: t('account_suspended', { keyPrefix: 'page_title' }),\n  });\n\n  return (\n    <div className=\"d-flex flex-column align-items-center mt-5 pt-3\">\n      <h3 className=\"mb-5\">{t('title')}</h3>\n      <p className=\"text-center\">\n        {t('forever')}\n        <br />\n        {t('end')}\n      </p>\n      <Button href={`mailto:${contact_email}`} variant=\"link\">\n        {t('contact_us')}\n      </Button>\n    </div>\n  );\n};\n\nexport default Suspended;\n"
  },
  {
    "path": "ui/src/pages/Users/Unsubscribe/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, memo, useEffect } from 'react';\nimport { Container, Row, Col } from 'react-bootstrap';\nimport { Link, useSearchParams } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { unsubscribe } from '@/services';\nimport { usePageTags } from '@/hooks';\n\nconst Index: FC = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'unsubscribe' });\n  usePageTags({\n    title: t('page_title'),\n  });\n  const [searchParams] = useSearchParams();\n  const code = searchParams.get('code');\n  useEffect(() => {\n    if (code) {\n      unsubscribe(code);\n    }\n  }, [code]);\n  return (\n    <Container className=\"pt-4 mt-2 mb-5\">\n      <Row className=\"justify-content-center\">\n        <Col lg={6}>\n          <h3 className=\"text-center mt-3 mb-5\">{t('success_title')}</h3>\n          <p className=\"text-center\">{t('success_desc')}</p>\n          <div className=\"text-center\">\n            <Link to=\"/users/settings/notify\">{t('link')}</Link>\n          </div>\n        </Col>\n      </Row>\n    </Container>\n  );\n};\n\nexport default memo(Index);\n"
  },
  {
    "path": "ui/src/pages/Users/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Row, Col } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\nimport { Link } from 'react-router-dom';\nimport { Fragment } from 'react';\n\nimport { usePageTags } from '@/hooks';\nimport { useQueryContributeUsers } from '@/services';\nimport { Avatar } from '@/components';\n\nconst Users = () => {\n  const { t } = useTranslation('translation', { keyPrefix: 'users' });\n\n  const { data: users } = useQueryContributeUsers();\n\n  usePageTags({\n    title: t('users', { keyPrefix: 'page_title' }),\n  });\n\n  if (!users) {\n    return null;\n  }\n\n  const keys = Object.keys(users);\n  return (\n    <Row className=\"py-4 mb-4 d-flex justify-content-center\">\n      <Col xxl={12}>\n        <h3 className=\"mb-4\">{t('title')}</h3>\n      </Col>\n\n      <Col xxl={12}>\n        {keys.map((key, index) => {\n          if (users[key]?.length === 0) {\n            return null;\n          }\n          return (\n            <Fragment key={key}>\n              <Row className=\"mb-4\">\n                <Col>\n                  <h6 className=\"mb-0\">{t(key)}</h6>\n                </Col>\n              </Row>\n              <Row className={index === keys.length - 1 ? '' : 'mb-4'}>\n                {users[key]?.map((user) => (\n                  <Col\n                    key={user.username}\n                    xl={3}\n                    lg={4}\n                    md={4}\n                    sm={6}\n                    xs={12}\n                    className=\"mb-4\">\n                    <div className=\"d-flex\">\n                      <Link to={`/users/${user.username}`}>\n                        <Avatar\n                          size=\"48px\"\n                          avatar={user?.avatar}\n                          searchStr=\"s=96\"\n                          alt={user.display_name}\n                        />\n                      </Link>\n                      <div className=\"ms-2\">\n                        <Link\n                          className=\"text-break\"\n                          to={`/users/${user.username}`}>\n                          {user.display_name}\n                        </Link>\n                        <div className=\"text-secondary small\">\n                          {key === 'users_with_the_most_vote'\n                            ? `${user.vote_count} ${t('votes')}`\n                            : `${user.rank} ${t('reputation')}`}\n                        </div>\n                      </div>\n                    </div>\n                  </Col>\n                ))}\n              </Row>\n            </Fragment>\n          );\n        })}\n      </Col>\n    </Row>\n  );\n};\n\nexport default Users;\n"
  },
  {
    "path": "ui/src/plugins/builtin/HostingConnector/i18n/en_US.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  hosting_connector:\n    ui:\n      connect: Connect with {{ auth_name }}\n      login: Login\n      qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.\n      login_failed_email_tip: Login failed, please allow this app to access your email information before try again.\n"
  },
  {
    "path": "ui/src/plugins/builtin/HostingConnector/i18n/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { initI18nResource } from '@/utils/pluginKit/utils';\n\nimport en_US from './en_US.yaml';\nimport zh_CN from './zh_CN.yaml';\n\ninitI18nResource({\n  en_US,\n  zh_CN,\n});\n"
  },
  {
    "path": "ui/src/plugins/builtin/HostingConnector/i18n/zh_CN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  hosting_connector:\n    ui:\n      connect: 连接到 {{ auth_name }}\n      login: 登录\n      qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录\n      login_failed_email_tip: 登录失败，请允许该应用程序访问您的电子邮件信息，然后再试一次。\n"
  },
  {
    "path": "ui/src/plugins/builtin/HostingConnector/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport {\n  getTransNs,\n  getTransKeyPrefix,\n  PluginInfo,\n} from '@/utils/pluginKit/utils';\nimport { SvgIcon } from '@/components';\nimport { userCenterStore } from '@/stores';\nimport './i18n';\n\nimport info from './info.yaml';\n\ninterface Props {\n  className?: classnames.Argument;\n}\n\nconst pluginInfo: PluginInfo = {\n  slug_name: info.slug_name,\n  type: info.type,\n};\n\nconst Index: FC<Props> = ({ className }) => {\n  const { t } = useTranslation(getTransNs(), {\n    keyPrefix: getTransKeyPrefix(pluginInfo),\n  });\n  const ucAgent = userCenterStore().agent;\n  const ucLoginRedirect =\n    ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;\n\n  if (ucLoginRedirect) {\n    return (\n      <Button\n        className={classnames('w-100', className)}\n        variant=\"outline-secondary\"\n        href={ucAgent?.agent_info.login_redirect_url}>\n        <SvgIcon base64={ucAgent?.agent_info.icon} svgClassName=\"btnSvg me-2\" />\n        <span>\n          {t('connect', { auth_name: ucAgent?.agent_info.display_name })}\n        </span>\n      </Button>\n    );\n  }\n  return null;\n};\nexport default {\n  info: pluginInfo,\n  component: memo(Index),\n};\n"
  },
  {
    "path": "ui/src/plugins/builtin/HostingConnector/info.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nslug_name: hosting_connector\ntype: connector\nversion: 0.0.1\nauthor: Answer\n\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/i18n/en_US.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  serarch_info:\n    ui:\n      search_by: Search by\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/i18n/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { initI18nResource } from '@/utils/pluginKit/utils';\n\nimport en_US from './en_US.yaml';\nimport zh_CN from './zh_CN.yaml';\n\ninitI18nResource({\n  en_US,\n  zh_CN,\n});\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/i18n/zh_CN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  serarch_info:\n    ui:\n      search_by: 搜索提供\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport {\n  getTransNs,\n  getTransKeyPrefix,\n  PluginInfo,\n} from '@/utils/pluginKit/utils';\nimport { SvgIcon } from '@/components';\n\nimport info from './info.yaml';\nimport { useGetSearchPLuginInfo } from './services';\nimport './i18n';\n\nconst pluginInfo: PluginInfo = {\n  slug_name: info.slug_name,\n  type: info.type,\n};\n\nconst Index: FC = () => {\n  const { t } = useTranslation(getTransNs(), {\n    keyPrefix: getTransKeyPrefix(pluginInfo),\n  });\n\n  const { data } = useGetSearchPLuginInfo();\n  if (!data?.icon) return null;\n\n  return (\n    <a\n      className=\"d-flex align-items-center\"\n      href={data?.link}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\">\n      <span className=\"small text-secondary me-2\">{t('search_by')}</span>\n      <SvgIcon base64={data?.icon} svgClassName=\"max-width-200\" />\n    </a>\n  );\n};\n\nexport default {\n  info: pluginInfo,\n  component: memo(Index),\n};\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/info.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nslug_name: serarch_info\ntype: search\nversion: 0.0.1\nauthor: Answer\n"
  },
  {
    "path": "ui/src/plugins/builtin/SearchInfo/services.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\n\nexport interface AlgoliaRes {\n  name: string;\n  icon: string;\n  link: string;\n}\n\nexport const useGetSearchPLuginInfo = () => {\n  const { data, error } = useSWR<AlgoliaRes>(\n    '/answer/api/v1/search/desc',\n    request.instance.get,\n  );\n\n  return {\n    data,\n    error,\n  };\n};\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/i18n/en_US.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  third_party_connector:\n    ui:\n      connect: Connect with {{ auth_name }}\n      remove: Remove {{ auth_name }}\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/i18n/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { initI18nResource } from '@/utils/pluginKit/utils';\n\nimport en_US from './en_US.yaml';\nimport zh_CN from './zh_CN.yaml';\n\ninitI18nResource({\n  en_US,\n  zh_CN,\n});\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/i18n/zh_CN.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nplugin:\n  third_party_connector:\n    ui:\n      connect: 连接到 {{ auth_name }}\n      remove: 解绑 {{ auth_name }}\n\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { memo, FC } from 'react';\nimport { Button } from 'react-bootstrap';\nimport { useTranslation } from 'react-i18next';\n\nimport classnames from 'classnames';\n\nimport {\n  getTransNs,\n  getTransKeyPrefix,\n  PluginInfo,\n} from '@/utils/pluginKit/utils';\nimport { SvgIcon } from '@/components';\n\nimport info from './info.yaml';\nimport { useGetStartUseOauthConnector } from './services';\nimport './i18n';\n\nconst pluginInfo: PluginInfo = {\n  slug_name: info.slug_name,\n  type: info.type,\n};\ninterface Props {\n  className?: string;\n}\nconst Index: FC<Props> = ({ className }) => {\n  const { t } = useTranslation(getTransNs(), {\n    keyPrefix: getTransKeyPrefix(pluginInfo),\n  });\n\n  const { data } = useGetStartUseOauthConnector();\n\n  if (!data?.length) return null;\n  return (\n    <div className={classnames('d-grid gap-2', className)}>\n      {data?.map((item) => {\n        return (\n          <Button variant=\"outline-secondary\" href={item.link} key={item.name}>\n            <SvgIcon base64={item.icon} svgClassName=\"btnSvg me-2\" />\n            <span>{t('connect', { auth_name: item.name })}</span>\n          </Button>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default {\n  info: pluginInfo,\n  component: memo(Index),\n};\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/info.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nslug_name: third_party_connector\ntype: connector\nversion: 0.0.1\nlink: https://github.com/apache/answer-plugins/tree/main/connector-basic\nauthor: Answer\n\n"
  },
  {
    "path": "ui/src/plugins/builtin/ThirdPartyConnector/services.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\n\nexport interface OauthConnectorItem {\n  icon: string;\n  name: string;\n  link: string;\n}\n\nexport const useGetStartUseOauthConnector = () => {\n  const { data, error } = useSWR<OauthConnectorItem[]>(\n    '/answer/api/v1/connector/info',\n    request.instance.get,\n  );\n\n  return {\n    data,\n    error,\n  };\n};\n"
  },
  {
    "path": "ui/src/plugins/builtin/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport ThirdPartyConnector from './ThirdPartyConnector';\nimport HostingConnector from './HostingConnector';\nimport SearchInfo from './SearchInfo';\n\nexport default {\n  ThirdPartyConnector,\n  HostingConnector,\n  SearchInfo,\n};\n"
  },
  {
    "path": "ui/src/plugins/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport default null;"
  },
  {
    "path": "ui/src/react-app-env.d.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n/// <reference types=\"react-scripts\" />\ndeclare module '*.yaml';\n\ndeclare module '*.ico';\n"
  },
  {
    "path": "ui/src/router/RouteErrorBoundary.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { HttpErrorContent } from '@/components';\n\nconst Index = ({ errCode = '50X', errMsg = '' }) => {\n  return <HttpErrorContent httpCode={errCode} errMsg={errMsg} />;\n};\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/router/RouteGuard.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { FC, ReactNode, useEffect, useState } from 'react';\nimport { useNavigate, useLoaderData } from 'react-router-dom';\n\nimport { floppyNavigation } from '@/utils';\nimport { TGuardFunc, TGuardResult } from '@/utils/guard';\n\nimport RouteErrorBoundary from './RouteErrorBoundary';\n\nconst RouteGuard: FC<{\n  children: ReactNode;\n  onEnter: TGuardFunc;\n  path?: string;\n  page?: string;\n}> = ({ children, onEnter, path, page }) => {\n  const navigate = useNavigate();\n  const loaderData = useLoaderData();\n  const [gk, setKeeper] = useState<TGuardResult>({\n    ok: true,\n  });\n  const [gkError, setGkError] = useState<TGuardResult['error']>();\n  const applyGuard = () => {\n    if (typeof onEnter !== 'function') {\n      return;\n    }\n\n    const gr = onEnter({\n      loaderData,\n      path,\n      page,\n    });\n\n    setKeeper(gr);\n    if (\n      gr.ok === false &&\n      gr.error?.code &&\n      /403|404|50X/i.test(gr.error.code.toString())\n    ) {\n      setGkError(gr.error);\n      return;\n    }\n    setGkError(undefined);\n    if (gr.redirect) {\n      floppyNavigation.navigate(gr.redirect, {\n        handler: navigate,\n        options: { replace: true },\n      });\n    }\n  };\n  useEffect(() => {\n    /**\n     * By detecting changes to location.href, many unnecessary tests can be avoided\n     */\n    applyGuard();\n  }, [window.location.href]);\n\n  let asOK = gk.ok;\n  if (gk.ok === false && gk.redirect) {\n    /**\n     * It is possible that the route guard verification fails\n     *    but the current page is already the target page for the route guard jump\n     * This should render `children`!\n     */\n    asOK = floppyNavigation.equalToCurrentHref(gk.redirect);\n  }\n  return (\n    <>\n      {asOK ? children : null}\n      {gkError ? <RouteErrorBoundary errCode={gkError.code as string} /> : null}\n    </>\n  );\n};\n\nexport default RouteGuard;\n"
  },
  {
    "path": "ui/src/router/alias.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport const REACT_BASE_PATH = process.env.REACT_APP_BASE_URL || '';\nexport const BASE_ORIGIN = `${window.location.origin}${REACT_BASE_PATH}`;\n\nexport const RouteAlias = {\n  home: '/',\n  login: '/users/login',\n  signUp: '/users/register',\n  inactive: '/users/login?status=inactive',\n  accountRecovery: '/users/account-recovery',\n  changeEmail: '/users/change-email',\n  passwordReset: '/users/password-reset',\n  accountActivation: '/users/account-activation',\n  activationSuccess: '/users/account-activation/success',\n  activationFailed: '/users/account-activation/failed',\n  suspended: '/users/account-suspended',\n  confirmNewEmail: '/users/confirm-new-email',\n  confirmEmail: '/users/confirm-email',\n  authLanding: '/users/auth-landing',\n};\n"
  },
  {
    "path": "ui/src/router/index.tsx",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Suspense, lazy, useEffect, useState } from 'react';\nimport { RouteObject } from 'react-router-dom';\n\nimport Layout from '@/pages/Layout';\nimport { mergeRoutePlugins } from '@/utils/pluginKit';\n\nimport baseRoutes, { RouteNode } from './routes';\nimport RouteGuard from './RouteGuard';\nimport RouteErrorBoundary from './RouteErrorBoundary';\n\nconst routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {\n  routeNodes.forEach((rn) => {\n    if (rn.page === 'pages/Layout') {\n      rn.element = rn.guard ? (\n        <RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>\n          <Layout />\n        </RouteGuard>\n      ) : (\n        <Layout />\n      );\n      rn.errorElement = <RouteErrorBoundary />;\n    } else {\n      /**\n       * cannot use a fully dynamic import statement\n       * ref: https://webpack.js.org/api/module-methods/#import-1\n       */\n\n      let Ctrl;\n\n      if (typeof rn.page === 'string') {\n        const pagePath = rn.page.replace('pages/', '');\n        Ctrl = lazy(() => import(`@/pages/${pagePath}`));\n      } else {\n        Ctrl = rn.page;\n      }\n\n      rn.element = (\n        <Suspense>\n          {rn.guard ? (\n            <RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>\n              <Ctrl />\n            </RouteGuard>\n          ) : (\n            <Ctrl />\n          )}\n        </Suspense>\n      );\n      rn.errorElement = <RouteErrorBoundary />;\n    }\n    root.push(rn);\n    const children = Array.isArray(rn.children) ? rn.children : null;\n    if (children) {\n      rn.children = [];\n      routeWrapper(children, rn.children);\n    }\n  });\n};\n\nfunction useMergeRoutes() {\n  const [routesState, setRoutes] = useState<RouteObject[]>([]);\n\n  const init = async () => {\n    const routes = [];\n    const mergedRoutes = await mergeRoutePlugins(baseRoutes).catch(() => []);\n    routeWrapper(mergedRoutes, routes);\n    setRoutes(routes);\n  };\n\n  useEffect(() => {\n    init();\n  }, []);\n\n  return routesState;\n}\n\nexport { useMergeRoutes };\n"
  },
  {
    "path": "ui/src/router/pathFactory.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { seoSettingStore } from '@/stores';\n\nconst tagLanding = (slugName: string) => {\n  const r = slugName ? `/tags/${encodeURIComponent(slugName)}` : '/tags';\n  return r;\n};\n\nconst tagInfo = (slugName: string) => {\n  const r = slugName ? `/tags/${encodeURIComponent(slugName)}/info` : '/tags';\n  return r;\n};\n\nconst tagEdit = (tagId: string) => {\n  const r = `/tags/${tagId}/edit`;\n  return r;\n};\n\nconst questionLanding = (questionId: string = '', slugTitle: string = '') => {\n  const { seo } = seoSettingStore.getState();\n  if (!questionId) {\n    return slugTitle ? `/questions/null/${slugTitle}` : '/questions/null';\n  }\n  // @ts-ignore\n  if (/[13]/.test(seo.permalink) && slugTitle) {\n    return `/questions/${questionId}/${encodeURIComponent(slugTitle)}`;\n  }\n\n  return `/questions/${questionId}`;\n};\n\nconst answerLanding = (params: {\n  questionId: string;\n  slugTitle?: string;\n  answerId: string;\n}) => {\n  const questionLandingUrl = questionLanding(\n    params.questionId,\n    params.slugTitle,\n  );\n  return `${questionLandingUrl}/${params.answerId}`;\n};\n\nexport const pathFactory = {\n  tagLanding,\n  tagInfo,\n  tagEdit,\n  questionLanding,\n  answerLanding,\n};\n"
  },
  {
    "path": "ui/src/router/routes.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom';\n\nimport { guard } from '@/utils';\nimport type { TGuardFunc } from '@/utils/guard';\nimport { editCheck } from '@/services';\nimport { isEditable } from '@/utils/guard';\n\ntype IndexRouteNode = Omit<IndexRouteObject, 'children'>;\ntype NonIndexRouteNode = Omit<NonIndexRouteObject, 'children'>;\ntype UnionRouteNode = IndexRouteNode | NonIndexRouteNode;\n\nexport type RouteNode = UnionRouteNode & {\n  page: string;\n  children?: RouteNode[];\n  /**\n   * a method to auto guard route before route enter\n   * if the `ok` field in guard returned `TGuardResult` is true,\n   * it means the guard passed then enter the route.\n   * if guard returned the `TGuardResult` has `redirect` field,\n   * then auto redirect route to the `redirect` target.\n   */\n  guard?: TGuardFunc;\n};\n\nconst routes: RouteNode[] = [\n  {\n    path: '/',\n    page: 'pages/Layout',\n    loader: async () => {\n      await guard.setupApp();\n      return null;\n    },\n    guard: () => {\n      const gr = guard.shouldLoginRequired();\n      if (!gr.ok) {\n        return gr;\n      }\n      return {\n        ok: true,\n      };\n    },\n    children: [\n      // question and answer\n      {\n        // side nav layout\n        page: 'pages/SideNavLayout',\n        children: [\n          {\n            index: true,\n            page: 'pages/Questions',\n          },\n          {\n            path: 'questions',\n            page: 'pages/Questions',\n          },\n          {\n            path: 'questions/ask',\n            page: 'pages/Questions/Ask',\n            guard: () => {\n              return guard.askRedirect();\n            },\n          },\n          {\n            path: 'questions/add',\n            page: 'pages/Questions/Ask',\n            guard: () => {\n              return guard.activated();\n            },\n          },\n          {\n            path: 'posts/:qid/edit',\n            page: 'pages/Questions/Ask',\n            guard: () => {\n              return guard.activated();\n            },\n          },\n          {\n            path: 'posts/:qid/:aid/edit',\n            page: 'pages/Questions/EditAnswer',\n            loader: async ({ params }) => {\n              const ret = await editCheck(params.aid as string, true);\n              return ret;\n            },\n            guard: (args) => {\n              return isEditable(args);\n            },\n          },\n          {\n            path: 'questions/:qid',\n            page: 'pages/Questions/Detail',\n          },\n          {\n            path: 'questions/:qid/:slugPermalink',\n            page: 'pages/Questions/Detail',\n          },\n          {\n            path: 'questions/:qid/:slugPermalink/:aid',\n            page: 'pages/Questions/Detail',\n          },\n          {\n            path: '/questions/linked/:qid',\n            page: 'pages/Questions/Linked',\n            guard: () => {\n              return guard.linkedRedirect();\n            },\n          },\n          {\n            path: '/linked/:qid',\n            page: 'pages/Questions/Linked',\n          },\n          {\n            path: '/search',\n            page: 'pages/Search',\n          },\n          // tags\n          {\n            path: 'tags',\n            page: 'pages/Tags',\n          },\n          {\n            path: 'tags/create',\n            page: 'pages/Tags/Create',\n            guard: () => {\n              return guard.isAdminOrModerator();\n            },\n          },\n          {\n            path: 'tags/:tagName',\n            page: 'pages/Tags/Detail',\n          },\n          {\n            path: 'tags/:tagName/questions',\n            page: 'pages/Tags/Detail',\n          },\n          {\n            path: 'tags/:tagName/info',\n            page: 'pages/Tags/Info',\n          },\n          {\n            path: 'tags/:tagId/edit',\n            page: 'pages/Tags/Edit',\n            guard: () => {\n              return guard.activated();\n            },\n          },\n          // for users\n          {\n            path: 'users',\n            page: 'pages/Users',\n          },\n          {\n            path: 'users/:username',\n            page: 'pages/Users/Personal',\n          },\n          {\n            path: 'users/:username/:tabName',\n            page: 'pages/Users/Personal',\n          },\n          {\n            path: 'users/settings',\n            page: 'pages/Users/Settings',\n            guard: () => {\n              return guard.logged();\n            },\n            children: [\n              {\n                index: true,\n                page: 'pages/Users/Settings/Profile',\n              },\n              {\n                path: 'profile',\n                page: 'pages/Users/Settings/Profile',\n              },\n              {\n                path: 'notify',\n                page: 'pages/Users/Settings/Notification',\n              },\n              {\n                path: 'account',\n                page: 'pages/Users/Settings/Account',\n              },\n              {\n                path: 'interface',\n                page: 'pages/Users/Settings/Interface',\n              },\n              {\n                path: ':slug_name',\n                page: 'pages/Users/Settings/Plugins',\n              },\n            ],\n          },\n          {\n            path: 'users/notifications/:type/:subType?',\n            page: 'pages/Users/Notifications',\n          },\n          {\n            path: '/posts/:qid/timeline',\n            page: 'pages/Timeline',\n            guard: () => {\n              return guard.logged();\n            },\n          },\n          {\n            path: '/posts/:qid/:aid/timeline',\n            page: 'pages/Timeline',\n            guard: () => {\n              return guard.logged();\n            },\n          },\n          {\n            path: '/tags/:tid/timeline',\n            page: 'pages/Timeline',\n            guard: () => {\n              return guard.logged();\n            },\n          },\n          // for review\n          {\n            path: 'review',\n            page: 'pages/Review',\n          },\n          {\n            path: '/badges',\n            page: 'pages/Badges/index',\n          },\n          {\n            path: '/badges/:badge_id',\n            page: 'pages/Badges/Detail/index',\n          },\n        ],\n      },\n      {\n        path: 'users/login',\n        page: 'pages/Users/Login',\n        guard: () => {\n          const notLogged = guard.notLogged();\n          if (notLogged.ok) {\n            return notLogged;\n          }\n\n          return guard.notActivated();\n        },\n      },\n      {\n        path: 'users/register',\n        page: 'pages/Users/Register',\n        guard: () => {\n          const allowNew = guard.allowNewRegistration();\n          if (!allowNew.ok) {\n            return allowNew;\n          }\n          const notLogged = guard.notLogged();\n          if (notLogged.ok) {\n            const sa = guard.singUpAgent();\n            if (!sa.ok) {\n              return sa;\n            }\n          }\n          return notLogged;\n        },\n      },\n      {\n        path: 'users/logout',\n        page: 'pages/Users/Logout',\n        guard: () => {\n          return guard.loggedRedirectHome();\n        },\n      },\n      {\n        path: 'users/account-recovery',\n        page: 'pages/Users/AccountForgot',\n        guard: () => {\n          return guard.notLogged();\n        },\n      },\n      {\n        path: 'users/change-email',\n        page: 'pages/Users/ChangeEmail',\n      },\n      {\n        path: 'users/password-reset',\n        page: 'pages/Users/PasswordReset',\n      },\n      {\n        path: 'users/account-activation',\n        page: 'pages/Users/ActiveEmail',\n      },\n      {\n        path: 'users/account-activation/success',\n        page: 'pages/Users/ActivationResult',\n        guard: () => {\n          return guard.activated();\n        },\n      },\n      {\n        path: '/users/account-activation/failed',\n        page: 'pages/Users/ActivationResult',\n        guard: () => {\n          return guard.notActivated();\n        },\n      },\n      {\n        path: '/users/confirm-new-email',\n        page: 'pages/Users/ConfirmNewEmail',\n      },\n      {\n        path: '/users/account-suspended',\n        page: 'pages/Users/Suspended',\n        guard: () => {\n          return guard.notLogged();\n        },\n      },\n      {\n        path: '/users/confirm-email',\n        page: 'pages/Users/OauthBindEmail',\n      },\n      {\n        path: '/users/auth-landing',\n        page: 'pages/Users/AuthCallback',\n      },\n      // for admin\n      {\n        path: 'admin',\n        page: 'pages/Admin',\n        loader: async () => {\n          await guard.pullLoggedUser();\n          return null;\n        },\n        guard: () => {\n          return guard.admin();\n        },\n        children: [\n          {\n            index: true,\n            page: 'pages/Admin/Dashboard',\n          },\n          {\n            path: 'dashboard',\n            page: 'pages/Admin/Dashboard',\n          },\n          {\n            path: 'qa/questions',\n            page: 'pages/Admin/Questions',\n          },\n          {\n            path: 'qa/answers',\n            page: 'pages/Admin/Answers',\n          },\n          {\n            path: 'qa/settings',\n            page: 'pages/Admin/QaSettings',\n          },\n          {\n            path: 'tags/settings',\n            page: 'pages/Admin/TagsSettings',\n          },\n          {\n            path: 'security',\n            page: 'pages/Admin/Security',\n          },\n          {\n            path: 'themes',\n            page: 'pages/Admin/Themes',\n          },\n          {\n            path: 'customize',\n            page: 'pages/Admin/CssAndHtml',\n          },\n          {\n            path: 'general',\n            page: 'pages/Admin/General',\n          },\n          {\n            path: 'interface',\n            page: 'pages/Admin/Interface',\n          },\n          {\n            path: 'users',\n            page: 'pages/Admin/Users',\n          },\n          {\n            path: 'users/settings',\n            page: 'pages/Admin/UsersSettings',\n          },\n          {\n            path: 'users/:user_id',\n            page: 'pages/Admin/UserOverview',\n          },\n          {\n            path: 'smtp',\n            page: 'pages/Admin/Smtp',\n          },\n          {\n            path: 'branding',\n            page: 'pages/Admin/Branding',\n          },\n          {\n            path: 'rules/policies',\n            page: 'pages/Admin/Policies',\n          },\n          {\n            path: 'files',\n            page: 'pages/Admin/Files',\n          },\n          {\n            path: 'seo',\n            page: 'pages/Admin/Seo',\n          },\n          {\n            path: 'login',\n            page: 'pages/Admin/Login',\n          },\n          {\n            path: 'rules/privileges',\n            page: 'pages/Admin/Privileges',\n          },\n          {\n            path: 'installed-plugins',\n            page: 'pages/Admin/Plugins/Installed',\n          },\n          {\n            path: ':slug_name',\n            page: 'pages/Admin/Plugins/Config',\n          },\n          {\n            path: 'badges',\n            page: 'pages/Admin/Badges',\n          },\n          {\n            path: 'ai-assistant',\n            page: 'pages/Admin/AiAssistant',\n          },\n          {\n            path: 'ai-settings',\n            page: 'pages/Admin/AiSettings',\n          },\n          {\n            path: 'apikeys',\n            page: 'pages/Admin/Apikeys',\n          },\n          {\n            path: 'mcp',\n            page: 'pages/Admin/Mcp',\n          },\n        ],\n      },\n      {\n        path: '/user-center/auth',\n        page: 'pages/UserCenter/Auth',\n        guard: () => {\n          const notLogged = guard.notLogged();\n          return notLogged;\n        },\n      },\n      {\n        path: '/user-center/auth-failed',\n        page: 'pages/UserCenter/AuthFailed',\n      },\n      {\n        path: '*',\n        page: 'pages/404',\n      },\n      {\n        path: '50x',\n        page: 'pages/50X',\n      },\n      // ai\n      {\n        page: 'pages/SideNavLayoutWithoutFooter',\n        children: [\n          {\n            path: '/ai-assistant',\n            page: 'pages/AiAssistant',\n            guard: () => {\n              return guard.logged();\n            },\n          },\n          {\n            path: '/ai-assistant/:id',\n            page: 'pages/AiAssistant',\n            guard: () => {\n              return guard.logged();\n            },\n          },\n        ],\n      },\n    ],\n  },\n  {\n    path: '/',\n    page: 'pages/Layout',\n    loader: async () => {\n      await guard.setupApp();\n      return null;\n    },\n    children: [\n      {\n        page: 'pages/SideNavLayout',\n        children: [\n          {\n            page: 'pages/Legal',\n            children: [\n              {\n                path: 'tos',\n                page: 'pages/Legal/Tos',\n              },\n              {\n                path: 'privacy',\n                page: 'pages/Legal/Privacy',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        path: '/users/unsubscribe',\n        page: 'pages/Users/Unsubscribe',\n      },\n      {\n        path: '403',\n        page: 'pages/403',\n      },\n    ],\n  },\n  {\n    path: '/install',\n    page: 'pages/Install',\n  },\n  {\n    path: '/maintenance',\n    page: 'pages/Maintenance',\n  },\n];\nexport default routes;\n"
  },
  {
    "path": "ui/src/services/admin/ai.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const getAiConfig = () => {\n  return request.get<Type.AiConfig>('/answer/admin/api/ai-config');\n};\n\nexport const useQueryAiProvider = () => {\n  const apiUrl = `/answer/admin/api/ai-provider`;\n  const { data, error, mutate } = useSWR<Type.AiProviderItem[], Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const checkAiConfig = (params) => {\n  return request.post('/answer/admin/api/ai-models', params);\n};\n\nexport const saveAiConfig = (params) => {\n  return request.put('/answer/admin/api/ai-config', params);\n};\n\nexport const useQueryAdminConversationDetail = (id: string) => {\n  const apiUrl = !id\n    ? null\n    : `/answer/admin/api/ai/conversation?conversation_id=${id}`;\n\n  const { data, error, mutate } = useSWR<Type.ConversationDetail, Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useQueryAdminConversationList = (params: Type.Paging) => {\n  const apiUrl = `/answer/admin/api/ai/conversation/page?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<\n    { count: number; list: Type.AdminConversationListItem[] },\n    Error\n  >(apiUrl, request.instance.get);\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const deleteAdminConversation = (id: string) => {\n  return request.delete('/answer/admin/api/ai/conversation', {\n    conversation_id: id,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/admin/answer.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useAnswerSearch = (\n  params: Type.AdminContentsReq & { question_id?: string },\n) => {\n  const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<Type.ListResult, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const changeAnswerStatus = (\n  answer_id: string,\n  status: Type.AdminAnswerStatus,\n) => {\n  return request.put('/answer/admin/api/answer/status', {\n    answer_id,\n    status,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/admin/apikeys.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useQueryApiKeys = () => {\n  const apiUrl = `/answer/admin/api/api-key/all`;\n  const { data, error, mutate } = useSWR<Type.AdminApiKeysItem[], Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const addApiKey = (params: Type.AddOrEditApiKeyParams) => {\n  return request.post('/answer/admin/api/api-key', params);\n};\n\nexport const updateApiKey = (params: Type.AddOrEditApiKeyParams) => {\n  return request.put('/answer/admin/api/api-key', params);\n};\n\nexport const deleteApiKey = (id: string) => {\n  return request.delete('/answer/admin/api/api-key', {\n    id,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/admin/badges.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useQueryBadges = (params) => {\n  const apiUrl = `/answer/admin/api/badges?${qs.stringify(params, { skipNulls: true })}`;\n  const { data, error, mutate } = useSWR<\n    Type.ListResult<Type.AdminBadgeListItem>,\n    Error\n  >(apiUrl, request.instance.get);\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const updateBadgeStatus = (params) => {\n  return request.put('/answer/admin/api/badge/status', params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/dashboard.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport * as Type from '@/common/interface';\nimport request from '@/utils/request';\n\nexport const useDashBoard = () => {\n  const apiUrl = `/answer/admin/api/dashboard`;\n  const { data, error } = useSWR<Type.AdminDashboard, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/admin/flag.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const putReport = (params) => {\n  return request.instance.put('/answer/admin/api/report', params);\n};\n\nexport const useFlagSearch = (params: Type.AdminFlagsReq) => {\n  const apiUrl = `/answer/admin/api/reports/page?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<Type.ListResult, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/admin/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport * from './answer';\nexport * from './flag';\nexport * from './question';\nexport * from './settings';\nexport * from './users';\nexport * from './dashboard';\nexport * from './plugins';\nexport * from './badges';\nexport * from './ai';\nexport * from './tags';\nexport * from './apikeys';\nexport * from './mcp';\n"
  },
  {
    "path": "ui/src/services/admin/mcp.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\n\ntype McpConfig = {\n  enabled: boolean;\n  type: string;\n  url: string;\n  http_header: string;\n};\n\nexport const getMcpConfig = () => {\n  return request.get<McpConfig>(`/answer/admin/api/mcp-config`);\n};\n\nexport const saveMcpConfig = (params: { enabled: boolean }) => {\n  return request.put(`/answer/admin/api/mcp-config`, params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/plugins.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport { UIOptions, UIWidget } from '@/components/SchemaForm';\n\nexport interface PluginOption {\n  label: string;\n  value: string;\n}\n\nexport interface PluginItem {\n  name: string;\n  type: UIWidget;\n  title: string;\n  description: string;\n  ui_options?: UIOptions;\n  options?: PluginOption[];\n  value?: string;\n  required?: boolean;\n}\n\nexport interface PluginConfig {\n  name: string;\n  slug_name: string;\n  config_fields: PluginItem[];\n}\n\nexport const useQueryPlugins = (params) => {\n  const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<any[], Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const updatePluginStatus = (params) => {\n  return request.put('/answer/admin/api/plugin/status', params);\n};\n\nexport const useQueryPluginConfig = (params) => {\n  const apiUrl = `/answer/admin/api/plugin/config?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<PluginConfig, Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const updatePluginConfig = (params) => {\n  return request.put('/answer/admin/api/plugin/config', params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/question.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useQuestionSearch = (params: Type.AdminContentsReq) => {\n  const apiUrl = `/answer/admin/api/question/page?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<Type.ListResult, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const changeQuestionStatus = (\n  question_id: string,\n  status: Type.AdminQuestionStatus,\n) => {\n  return request.put('/answer/admin/api/question/status', {\n    question_id,\n    status,\n  });\n};\n\nexport const getQuestionSetting = () => {\n  return request.get<Type.AdminQuestionSetting>(\n    '/answer/admin/api/siteinfo/question',\n  );\n};\n\nexport const updateQuestionSetting = (params: Type.AdminQuestionSetting) => {\n  return request.put('/answer/admin/api/siteinfo/question', params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/settings.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport interface AdminSettingsUsers {\n  allow_update_avatar: boolean;\n  allow_update_bio: boolean;\n  allow_update_display_name: boolean;\n  allow_update_location: boolean;\n  allow_update_username: boolean;\n  allow_update_website: boolean;\n}\n\ninterface PrivilegeLevel {\n  level: number;\n  level_desc: string;\n  privileges: {\n    label: string;\n    value: number;\n    key: string;\n  }[];\n}\nexport interface AdminSettingsPrivilege {\n  selected_level: number;\n  options: PrivilegeLevel[];\n}\n\nexport interface AdminSettingsPrivilegeReq {\n  level: number;\n  custom_privileges?: {\n    label?: string;\n    value: number;\n    key: string;\n  }[];\n}\n\nexport const useGeneralSetting = () => {\n  const apiUrl = `/answer/admin/api/siteinfo/general`;\n  const { data, error } = useSWR<Type.AdminSettingsGeneral, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const updateGeneralSetting = (params: Type.AdminSettingsGeneral) => {\n  const apiUrl = `/answer/admin/api/siteinfo/general`;\n  return request.put(apiUrl, params);\n};\n\nexport const useInterfaceSetting = () => {\n  const apiUrl = `/answer/admin/api/siteinfo/interface`;\n  const { data, error } = useSWR<Type.AdminSettingsInterface, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const updateInterfaceSetting = (params: Type.AdminSettingsInterface) => {\n  const apiUrl = `/answer/admin/api/siteinfo/interface`;\n  return request.put(apiUrl, params);\n};\n\nexport const useSmtpSetting = () => {\n  const apiUrl = `/answer/admin/api/setting/smtp`;\n  const { data, error } = useSWR<Type.AdminSettingsSmtp, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => {\n  const apiUrl = `/answer/admin/api/setting/smtp`;\n  return request.put(apiUrl, params);\n};\n\nexport const getAdminLanguageOptions = () => {\n  const apiUrl = `/answer/admin/api/language/options`;\n  return request.get<Type.LangsType[]>(apiUrl);\n};\n\nexport const getBrandSetting = () => {\n  return request.get('/answer/admin/api/siteinfo/branding');\n};\n\nexport const brandSetting = (params: Type.AdminSettingBranding) => {\n  return request.put('/answer/admin/api/siteinfo/branding', params);\n};\n\nexport const getAdminFilesSetting = () => {\n  return request.get('/answer/admin/api/siteinfo/advanced');\n};\n\nexport const updateAdminFilesSetting = (params: Type.AdminSettingsWrite) => {\n  return request.put('/answer/admin/api/siteinfo/advanced', params);\n};\n\nexport const getSeoSetting = () => {\n  return request.get<Type.AdminSettingsSeo>('/answer/admin/api/siteinfo/seo');\n};\n\nexport const putSeoSetting = (params: Type.AdminSettingsSeo) => {\n  return request.put('/answer/admin/api/siteinfo/seo', params);\n};\n\nexport const getThemeSetting = () => {\n  return request.get<Type.AdminSettingsTheme>(\n    '/answer/admin/api/siteinfo/theme',\n  );\n};\n\nexport const putThemeSetting = (params: Type.AdminSettingsTheme) => {\n  return request.put('/answer/admin/api/siteinfo/theme', params);\n};\n\nexport const getPageCustom = () => {\n  return request.get<Type.AdminSettingsCustom>(\n    '/answer/admin/api/siteinfo/custom-css-html',\n  );\n};\n\nexport const putPageCustom = (params: Type.AdminSettingsCustom) => {\n  return request.put('/answer/admin/api/siteinfo/custom-css-html', params);\n};\n\nexport const getLoginSetting = () => {\n  return request.get<Type.AdminSettingsLogin>(\n    '/answer/admin/api/siteinfo/login',\n  );\n};\n\nexport const putLoginSetting = (params: Type.AdminSettingsLogin) => {\n  return request.put('/answer/admin/api/siteinfo/login', params);\n};\n\nexport const getUsersSetting = () => {\n  return request.get<AdminSettingsUsers>('/answer/admin/api/siteinfo/users');\n};\n\nexport const putUsersSetting = (params: AdminSettingsUsers) => {\n  return request.put('/answer/admin/api/siteinfo/users', params);\n};\n\nexport const getPrivilegeSetting = () => {\n  return request.get<AdminSettingsPrivilege>(\n    '/answer/admin/api/setting/privileges',\n  );\n};\n\nexport const putPrivilegeSetting = (params: AdminSettingsPrivilegeReq) => {\n  return request.put('/answer/admin/api/setting/privileges', params);\n};\n\nexport const getPoliciesSetting = () => {\n  return request.get<Type.AdminSettingsLegal>(\n    '/answer/admin/api/siteinfo/polices',\n  );\n};\n\nexport const putPoliciesSetting = (params: Type.AdminSettingsLegal) => {\n  return request.put('/answer/admin/api/siteinfo/polices', params);\n};\n\nexport const getSecuritySetting = () => {\n  return request.get<Type.AdminSettingsSecurity>(\n    '/answer/admin/api/siteinfo/security',\n  );\n};\n\nexport const putSecuritySetting = (params: Type.AdminSettingsSecurity) => {\n  return request.put('/answer/admin/api/siteinfo/security', params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/tags.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const getAdminTagsSetting = () => {\n  return request.get<Type.AdminTagsSetting>('/answer/admin/api/siteinfo/tag');\n};\n\nexport const updateAdminTagsSetting = (params: Type.AdminTagsSetting) => {\n  return request.put('/answer/admin/api/siteinfo/tag', params);\n};\n"
  },
  {
    "path": "ui/src/services/admin/users.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const changeUserStatus = (params) => {\n  return request.put('/answer/admin/api/user/status', params);\n};\n\nexport const useQueryUsers = (params) => {\n  const apiUrl = `/answer/admin/api/users/page?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<Type.ListResult, Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const getUserRoles = () => {\n  return request.get('/answer/admin/api/roles');\n};\n\nexport const changeUserRole = (params) => {\n  return request.put('/answer/admin/api/user/role', params);\n};\n\nexport const addUser = (params: {\n  display_name: string;\n  email: string;\n  password: string;\n}) => {\n  return request.post('/answer/admin/api/user', params);\n};\n\nexport const addUsers = (params: { users: string }) => {\n  return request.post('/answer/admin/api/users', params);\n};\n\nexport const updateUserPassword = (params: {\n  password: string;\n  user_id: string;\n}) => {\n  return request.put('/answer/admin/api/user/password', params);\n};\n\nexport const updateUserProfile = (params: {\n  display_name: string;\n  username: string;\n  email: string;\n  user_id: string;\n}) => {\n  return request.put('/answer/admin/api/user/profile', params);\n};\n\nexport const getUserActivation = (userId: string) => {\n  const apiUrl = `/answer/admin/api/user/activation`;\n  return request.get<{\n    activation_url: string;\n  }>(apiUrl, {\n    params: {\n      user_id: userId,\n    },\n  });\n};\n\nexport const postUserActivation = (userId: string) => {\n  const apiUrl = `/answer/admin/api/user/activation`;\n  return request.post(apiUrl, {\n    user_id: userId,\n  });\n};\n\nexport const useAdminUsersSettings = () => {\n  const apiUrl = `/answer/admin/api/siteinfo/users-settings`;\n  const { data, error } = useSWR<\n    {\n      default_avatar: string;\n      gravatar_base_url: string;\n    },\n    Error\n  >(apiUrl, request.instance.get);\n  return { data, isLoading: !data && !error, error };\n};\n\nexport const updateAdminUsersSettings = (params: {\n  default_avatar: string;\n  gravatar_base_url: string;\n}) => {\n  return request.put('/answer/admin/api/siteinfo/users-settings', params);\n};\n"
  },
  {
    "path": "ui/src/services/client/Oauth.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const oAuthBindEmail = (data: Type.OauthBindEmailReq) => {\n  return request.post('/answer/api/v1/connector/binding/email', data);\n};\n\nexport const useOauthConnectorInfoByUser = () => {\n  const { data, error, mutate } = useSWR<Type.UserOauthConnectorItem[]>(\n    '/answer/api/v1/connector/user/info',\n    request.instance.get,\n  );\n  return {\n    data,\n    mutate,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const userOauthUnbind = (data: { external_id: string }) => {\n  return request.delete('/answer/api/v1/connector/user/unbinding', data);\n};\n"
  },
  {
    "path": "ui/src/services/client/activity.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useFollow = (params?: Type.FollowParams) => {\n  const apiUrl = '/answer/api/v1/follow';\n  const { data, error, mutate } = useSWR<any, Error>(\n    params ? [apiUrl, params] : null,\n    request.instance.post,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/client/ai.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const getConversationList = (params: Type.Paging) => {\n  return request.get<{ count: number; list: Type.ConversationListItem[] }>(\n    `/answer/api/v1/ai/conversation/page?${qs.stringify(params)}`,\n  );\n};\n\nexport const getConversationDetail = (id: string) => {\n  return request.get<Type.ConversationDetail>(\n    `/answer/api/v1/ai/conversation?conversation_id=${id}`,\n  );\n};\n\n// /answer/api/v1/ai/conversation/vote\nexport const voteConversation = (params: Type.VoteConversationParams) => {\n  return request.post('/answer/api/v1/ai/conversation/vote', params);\n};\n"
  },
  {
    "path": "ui/src/services/client/badges.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useGetAllBadges = () => {\n  const apiUrl = '/answer/api/v1/badges';\n  const { data, error, mutate } = useSWR<Array<Type.BadgeListGroupItem>, Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useGetBadgeInfo = (id: string) => {\n  const { data, error, mutate } = useSWR<Type.BadgeInfo, Error>(\n    `/answer/api/v1/badge?id=${id}`,\n    (url) =>\n      request.get(url, {\n        allow404: true,\n      }),\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useBadgeDetailList = (params: Type.BadgeDetailListReq) => {\n  const path = params.badge_id\n    ? `/answer/api/v1/badge/awards/page?${qs.stringify(params, {\n        skipNulls: true,\n      })}`\n    : null;\n  const { data, error, mutate } = useSWR<Type.BadgeDetailListRes, Error>(\n    path,\n    (url) =>\n      request.get(url, {\n        allow404: true,\n      }),\n  );\n\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useGetRecentAwardBadges = (username) => {\n  const apiUrl = username\n    ? `/answer/api/v1/badge/user/awards/recent?username=${username}`\n    : null;\n  const { data, error, mutate } = useSWR<\n    { count: number; list: Array<Type.BadgeListItem> },\n    Error\n  >(apiUrl, request.instance.get);\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/client/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport * from './activity';\nexport * from './personal';\nexport * from './notification';\nexport * from './question';\nexport * from './search';\nexport * from './tag';\nexport * from './settings';\nexport * from './legal';\nexport * from './timeline';\nexport * from './revision';\nexport * from './user';\nexport * from './Oauth';\nexport * from './review';\nexport * from './badges';\nexport * from './ai';\n"
  },
  {
    "path": "ui/src/services/client/legal.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useLegalTos = () => {\n  const apiUrl = '/answer/api/v1/siteinfo/legal?info_type=tos';\n  const { data, error, mutate } = useSWR<Type.AdminSettingsLegal, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useLegalPrivacy = () => {\n  const apiUrl = '/answer/api/v1/siteinfo/legal?info_type=privacy';\n  const { data, error, mutate } = useSWR<Type.AdminSettingsLegal, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/client/notification.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\nimport { tryLoggedAndActivated } from '@/utils/guard';\n\nexport const useQueryNotifications = (params) => {\n  const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, {\n    skipNulls: true,\n  })}`;\n\n  const { data, error, mutate } = useSWR<Type.ListResult>(\n    apiUrl,\n    request.instance.get,\n  );\n\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const readNotification = (id) => {\n  return request.instance.put('/answer/api/v1/notification/read/state', {\n    id,\n  });\n};\n\nexport const useQueryNotificationStatus = () => {\n  const apiUrl = '/answer/api/v1/notification/status';\n\n  return useSWR<Type.NotificationStatus>(\n    tryLoggedAndActivated().ok ? apiUrl : null,\n    (url) => request.get(url, { ignoreError: '50X' }),\n    {\n      refreshInterval: 3000,\n    },\n  );\n};\n\nexport const clearNotificationStatus = (type) => {\n  return request.instance.put('/answer/api/v1/notification/status', {\n    type,\n  });\n};\n\nexport const clearUnreadNotification = (type) => {\n  return request.instance.put('/answer/api/v1/notification/read/state/all', {\n    type,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/client/personal.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const usePersonalInfoByName = (username: string) => {\n  const apiUrl = '/answer/api/v1/personal/user/info';\n  const { data, error, mutate } = useSWR<Type.UserInfoRes, Error>(\n    username ? `${apiUrl}?username=${username}` : null,\n    (url) =>\n      request.get(url, {\n        allow404: true,\n      }),\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\ninterface ListReq {\n  username?: string;\n  page: number;\n  page_size: number;\n  order?: string;\n}\n\ninterface ListRes {\n  count: number;\n  list: any[];\n}\n\nexport const usePersonalTop = (username: string, tabName: string) => {\n  const apiUrl = '/answer/api/v1/personal/qa/top?username=';\n  const { data, error } = useSWR<{ answer: any[]; question: any[] }, Error>(\n    tabName === 'overview' ? `${apiUrl}${username}` : null,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const usePersonalListByTabName = (params: ListReq, tabName: string) => {\n  let apiUrl: string | null = '';\n  if (tabName === 'answers') {\n    apiUrl = '/answer/api/v1/personal/answer/page';\n  }\n  if (tabName === 'questions') {\n    apiUrl = '/answer/api/v1/personal/question/page';\n  }\n  if (tabName === 'bookmarks') {\n    delete params.order;\n    apiUrl = '/answer/api/v1/personal/collection/page';\n  }\n  if (tabName === 'comments') {\n    delete params.order;\n    apiUrl = '/answer/api/v1/personal/comment/page';\n  }\n  if (tabName === 'reputation') {\n    delete params.order;\n    apiUrl = '/answer/api/v1/personal/rank/page';\n  }\n  if (tabName === 'votes') {\n    delete params.username;\n    apiUrl = '/answer/api/v1/personal/vote/page';\n  }\n  if (tabName === 'badges') {\n    delete params.order;\n    apiUrl = '/answer/api/v1/badge/user/awards';\n  }\n\n  const queryParams = qs.stringify(params, { skipNulls: true });\n  const { data, error, mutate } = useSWR<ListRes, Error>(\n    tabName !== 'overview' ? `${apiUrl}?${queryParams}` : null,\n    request.instance.get,\n  );\n\n  return {\n    data: {\n      [tabName]: data,\n    },\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n"
  },
  {
    "path": "ui/src/services/client/question.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useQuestionList = (params: Type.QueryQuestionsReq) => {\n  const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`;\n  const { data, error } = useSWR<Type.ListResult, Error>(apiUrl, (url) =>\n    request.get(url, { allow404: true }),\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const useQuestionRecommendList = (params: Type.QueryQuestionsReq) => {\n  const apiUrl = `/answer/api/v1/question/recommend/page?${qs.stringify(\n    params,\n  )}`;\n  const { data, error } = useSWR<Type.ListResult, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const useHotQuestions = (\n  params: Type.QueryQuestionsReq = {\n    page: 1,\n    page_size: 6,\n    order: 'hot',\n    in_days: 7,\n  },\n) => {\n  const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`;\n  const { data, error } = useSWR<Type.ListResult, Error>(\n    [apiUrl],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const useSimilarQuestion = (params: {\n  question_id: string;\n  page_size: number;\n}) => {\n  const apiUrl = `/answer/api/v1/question/similar/tag?${qs.stringify(params)}`;\n\n  const { data, error } = useSWR<Type.ListResult, Error>(\n    params.question_id ? apiUrl : null,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const getInviteUser = (questionId: string) => {\n  const apiUrl = '/answer/api/v1/question/invite';\n  return request.get<Type.UserInfoBase[]>(apiUrl, {\n    params: { id: questionId },\n  });\n};\n\nexport const putInviteUser = (\n  questionId: string,\n  users: string[],\n  imgCode: Type.ImgCodeReq = {},\n) => {\n  const apiUrl = '/answer/api/v1/question/invite';\n  return request.put(apiUrl, {\n    id: questionId,\n    invite_user: users,\n    ...imgCode,\n  });\n};\n\nexport const unDeleteAnswer = (id) => {\n  return request.post('/answer/api/v1/answer/recover', {\n    answer_id: id,\n  });\n};\n\nexport const unDeleteQuestion = (qid) => {\n  return request.post('/answer/api/v1/question/recover', {\n    question_id: qid,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/client/review.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\n// import useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport * as Type from '@/common/interface';\n\nexport const getSuggestReviewList = (page: number) => {\n  const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;\n  return request.get<Type.SuggestReviewResp>(apiUrl);\n};\n\nexport const getReviewType = () => {\n  return request.get<Type.ReviewTypeItem[]>('/answer/api/v1/reviewing/type');\n};\n\nexport const getFlagReviewPostList = (page: number) => {\n  const apiUrl = `/answer/api/v1/report/unreviewed/post?page=${page}`;\n  return request.get<Type.FlagReviewResp>(apiUrl);\n};\n\nexport const putFlagReviewAction = (params: Type.PutFlagReviewParams) => {\n  return request.put('/answer/api/v1/report/review', params);\n};\n\nexport const getPendingReviewPostList = (page: number, objectId?: string) => {\n  const search = objectId ? `&object_id=${objectId}` : '';\n  const apiUrl = `/answer/api/v1/review/pending/post/page?page=${page}${search}`;\n  return request.get<Type.QueuedReviewResp>(apiUrl);\n};\n\nexport const putPendingReviewAction = (params: {\n  review_id: number;\n  status: 'approve' | 'reject';\n}) => {\n  return request.put('/answer/api/v1/review/pending/post', params);\n};\n"
  },
  {
    "path": "ui/src/services/client/revision.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\n\nexport const editCheck = (id: string, passingError: boolean = false) => {\n  const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`;\n  return request.get(apiUrl, {\n    passingError,\n  });\n};\n\nexport const revisionAudit = (id: string, operation: 'approve' | 'reject') => {\n  const apiUrl = `/answer/api/v1/revisions/audit`;\n  return request.put(apiUrl, {\n    id,\n    operation,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/client/search.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const getSearchResult = (params?: Type.SearchParams) => {\n  const apiUrl = '/answer/api/v1/search';\n\n  return request.get<Type.SearchRes>(apiUrl, {\n    params,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/client/settings.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\nimport type { PluginConfig } from '@/services/admin/plugins';\n\nexport const getLanguageConfig = () => {\n  return request.get('/answer/api/v1/language/config');\n};\n\nexport const getLanguageOptions = () => {\n  return request.get<Type.LangsType[]>('/answer/api/v1/language/options');\n};\n\ninterface userSettingInterface {\n  language: '';\n  color_scheme: '';\n}\nexport const updateUserInterface = (data: userSettingInterface) => {\n  return request.put('/answer/api/v1/user/interface', data);\n};\n\nexport const useGetNotificationConfig = () => {\n  return useSWR<Type.NotificationConfig>(\n    '/answer/api/v1/user/notification/config',\n    request.instance.get,\n  );\n};\n\nexport const putNotificationConfig = (data: Type.NotificationConfig) => {\n  return request.put('/answer/api/v1/user/notification/config', data);\n};\n\nexport const useGetUserPluginList = () => {\n  return useSWR<Type.UserPluginsConfigRes[]>(\n    '/answer/api/v1/user/plugin/configs',\n    request.instance.get,\n  );\n};\n\nexport const useGetUserPluginConfig = (params) => {\n  const apiUrl = `/answer/api/v1/user/plugin/config?${qs.stringify(params)}`;\n  const { data, error, mutate } = useSWR<PluginConfig, Error>(\n    apiUrl,\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const updateUserPluginConfig = (params) => {\n  return request.put('/answer/api/v1/user/plugin/config', params);\n};\n"
  },
  {
    "path": "ui/src/services/client/tag.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\nimport { tryLoggedAndActivated } from '@/utils/guard';\n\nexport const deleteTag = (id) => {\n  return request.delete('/answer/api/v1/tag', {\n    tag_id: id,\n  });\n};\nexport const modifyTag = (params) => {\n  return request.put('/answer/api/v1/tag', params);\n};\nexport const mergeTag = (params: {\n  source_tag_id: string;\n  target_tag_id: string;\n}) => {\n  return request.post('/answer/api/v1/tag/merge', params);\n};\n\nexport const useQuerySynonymsTags = (tagId, status) => {\n  const apiUrl =\n    status === 'deleted'\n      ? ''\n      : tagId\n        ? `/answer/api/v1/tag/synonyms?tag_id=${tagId}`\n        : '';\n  return useSWR<{\n    synonyms: Type.SynonymsTag[];\n    member_actions?: Type.MemberActionItem[];\n  }>(apiUrl, request.instance.get);\n};\n\nexport const saveSynonymsTags = (params) => {\n  return request.put('/answer/api/v1/tag/synonym', params);\n};\n\nexport const useFollowingTags = () => {\n  let apiUrl = '';\n  if (tryLoggedAndActivated().ok) {\n    apiUrl = '/answer/api/v1/tags/following';\n  }\n  const { data, error, mutate } = useSWR<any[]>(apiUrl, request.instance.get);\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useTagInfo = ({ id = '', name = '' }) => {\n  let apiUrl;\n  if (id) {\n    apiUrl = `/answer/api/v1/tag?id=${id}`;\n  } else if (name) {\n    name = encodeURIComponent(name);\n    apiUrl = `/answer/api/v1/tag?name=${name}`;\n  }\n  const { data, error, mutate } = useSWR<Type.TagInfo>(apiUrl, (url) =>\n    request.get(url, { allow404: true }),\n  );\n  return {\n    mutate,\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const followTags = (params) => {\n  return request.put('/answer/api/v1/follow/tags', params);\n};\n\nexport const getTagsBySlugName = (slugNames: string) => {\n  const apiUrl = `/answer/api/v1/tags?tags=${encodeURIComponent(slugNames)}`;\n  return request.get<Type.TagInfo[]>(apiUrl);\n};\n\nexport const createTag = (params: Type.TagBase) => {\n  const apiUrl = '/answer/api/v1/tag';\n  return request.post<Type.TagInfo>(apiUrl, params);\n};\n\nexport const unDeleteTag = (id) => {\n  return request.post('/answer/api/v1/tag/recover', {\n    tag_id: id,\n  });\n};\n"
  },
  {
    "path": "ui/src/services/client/timeline.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\n// export const useTimelineData = (params: Type.TimelineReq) => {\n//   const apiUrl = '/answer/api/v1/activity/timeline';\n//   const { data, error, mutate } = useSWR<Type.TimelineRes, Error>(\n//     `${apiUrl}?${qs.stringify(params, { skipNulls: true })}`,\n//     request.instance.get,\n//   );\n//   return {\n//     data,\n//     isLoading: !data && !error,\n//     error,\n//     mutate,\n//   };\n// };\n\nexport const getTimelineData = (params: Type.TimelineReq) => {\n  return request.get<Type.TimelineRes>(\n    `/answer/api/v1/activity/timeline?${qs.stringify(params, {\n      skipNulls: true,\n    })}`,\n  );\n};\n\nexport const getTimelineDetail = (params: {\n  new_revision_id: string;\n  old_revision_id: string;\n}) => {\n  return request.get(\n    `/answer/api/v1/activity/timeline/detail?${qs.stringify(params, {\n      skipNulls: true,\n    })}`,\n  );\n};\n"
  },
  {
    "path": "ui/src/services/client/user.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const useQueryContributeUsers = () => {\n  const apiUrl = '/answer/api/v1/user/ranking';\n  return useSWR<{\n    users_with_the_most_reputation: Type.User[];\n    users_with_the_most_vote: Type.User[];\n    staffs: Type.User[];\n  }>(apiUrl, request.instance.get);\n};\n\nexport const userSearchByName = (name: string) => {\n  const apiUrl = '/answer/api/v1/user/info/search';\n  return request.get<Type.UserInfoBase[]>(apiUrl, {\n    params: {\n      username: name,\n    },\n  });\n};\n\nexport type UserPermissionKey =\n  | 'question.add'\n  | 'question.edit'\n  | 'question.edit_without_review'\n  | 'question.delete'\n  | 'question.close'\n  | 'question.reopen'\n  | 'question.vote_up'\n  | 'question.vote_down'\n  | 'question.pin'\n  | 'question.unpin'\n  | 'question.hide'\n  | 'question.show'\n  | 'answer.add'\n  | 'answer.edit'\n  | 'answer.edit_without_review'\n  | 'answer.delete'\n  | 'answer.accept'\n  | 'answer.vote_up'\n  | 'answer.vote_down'\n  | 'answer.invite_someone_to_answer'\n  | 'comment.add'\n  | 'comment.edit'\n  | 'comment.delete'\n  | 'comment.vote_up'\n  | 'comment.vote_down'\n  | 'report.add'\n  | 'tag.add'\n  | 'tag.edit'\n  | 'tag.edit_slug_name'\n  | 'tag.edit_without_review'\n  | 'tag.delete, tag.synonym'\n  | 'link.url_limit'\n  | 'vote.detail'\n  | 'answer.audit'\n  | 'question.audit'\n  | 'tag.audit'\n  | 'tag.use_reserved_tag';\nexport const useUserPermission = (\n  keys: UserPermissionKey | UserPermissionKey[],\n) => {\n  const apiUrl = '/answer/api/v1/permission';\n  const action = Array.isArray(keys) ? keys.join(',') : keys;\n\n  return useSWR<\n    Partial<\n      Record<\n        UserPermissionKey,\n        {\n          has_permission: boolean;\n          no_permission_tip: string;\n        }\n      >\n    >\n  >([apiUrl, { params: { action } }], request.instance.get);\n};\n\nexport const useSearchUserStaff = (name: string) => {\n  const apiUrl = name\n    ? `/answer/api/v1/user/staff?username=${name}&page_size=10`\n    : null;\n  return useSWR<Type.User[]>(apiUrl, request.instance.get);\n};\n"
  },
  {
    "path": "ui/src/services/common.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport qs from 'qs';\nimport useSWR from 'swr';\n\nimport request from '@/utils/request';\nimport type * as Type from '@/common/interface';\n\nexport const uploadImage = (params: { file: File; type: Type.UploadType }) => {\n  const form = new FormData();\n  form.append('source', String(params.type));\n  form.append('file', params.file);\n  return request.post('/answer/api/v1/file', form);\n};\n\nexport const queryQuestionByTitle = (title: string) => {\n  return request.get(`/answer/api/v1/question/similar?title=${title}`);\n};\n\nexport const useQueryTags = (params) => {\n  const apiUrl = `/answer/api/v1/tags/page?${qs.stringify(params, {\n    skipNulls: true,\n  })}`;\n  const { data, error, mutate } = useSWR<Type.ListResult>(apiUrl, (url) =>\n    request.get(url, { allow404: true }),\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n    mutate,\n  };\n};\n\nexport const useQueryRevisions = (object_id: string | undefined) => {\n  return useSWR<Record<string, any>>(\n    object_id ? `/answer/api/v1/revisions?object_id=${object_id}` : '',\n    request.instance.get,\n  );\n};\n\nexport const useQueryComments = (params) => {\n  if (params.page === 0) {\n    params.query_cond = 'vote';\n    params.page = 1;\n  } else {\n    // only first page need commentId\n    params.query_cond = '';\n    delete params.comment_id;\n  }\n  return useSWR<Type.ListResult>(\n    `/answer/api/v1/comment/page?${qs.stringify(params, {\n      skipNulls: true,\n    })}`,\n    request.instance.get,\n  );\n};\n\nexport const updateComment = (params) => {\n  return request.put('/answer/api/v1/comment', params);\n};\n\nexport const deleteComment = (id, imgCode: Type.ImgCodeReq = {}) => {\n  return request.delete('/answer/api/v1/comment', {\n    comment_id: id,\n    ...imgCode,\n  });\n};\n\nexport const addComment = (params) => {\n  return request.post('/answer/api/v1/comment', params);\n};\n\nexport const updateReaction = (params) => {\n  return request.put('/answer/api/v1/meta/reaction', params);\n};\n\nexport const queryReactions = (object_id: string) => {\n  return request.get<Type.ReactionItems>(\n    `/answer/api/v1/meta/reaction?object_id=${object_id}`,\n  );\n};\n\nexport const queryTags = (tag: string) => {\n  return request.get(\n    `/answer/api/v1/question/tags?tag=${encodeURIComponent(tag)}`,\n  );\n};\n\nexport const useQueryAnswerInfo = (id: string) => {\n  return useSWR<{\n    info;\n    question;\n  }>(`/answer/api/v1/answer/info?id=${id}`, request.instance.get);\n};\n\nexport const modifyQuestion = (\n  params: Type.QuestionParams & { id: string; edit_summary: string },\n) => {\n  return request.put(`/answer/api/v1/question`, params);\n};\n\nexport const modifyAnswer = (params: Type.AnswerParams) => {\n  return request.put(`/answer/api/v1/answer`, params);\n};\n\nexport const login = (params: Type.LoginReqParams) => {\n  return request.post<Type.UserInfoRes>(\n    '/answer/api/v1/user/login/email',\n    params,\n  );\n};\n\nexport const register = (params: Type.RegisterReqParams) => {\n  return request.post<any>('/answer/api/v1/user/register/email', params);\n};\n\nexport const logout = () => {\n  return request.get('/answer/api/v1/user/logout');\n};\n\nexport const resendEmail = (params?: Type.ImgCodeReq) => {\n  params = qs.parse(\n    qs.stringify(params, {\n      skipNulls: true,\n    }),\n  );\n  return request.post('/answer/api/v1/user/email/verification/send', {\n    ...params,\n  });\n};\n\n/**\n * @description get login userinfo\n * @returns {UserInfo}\n */\nexport const getLoggedUserInfo = (config = { passingError: false }) => {\n  return request.get<Type.UserInfoRes>('/answer/api/v1/user/info', config);\n};\n\nexport const modifyUserInfo = (params: Type.ModifyUserReq) => {\n  return request.put('/answer/api/v1/user/info', params);\n};\n\nexport const modifyPassword = (params: Type.ModifyPasswordReq) => {\n  return request.put('/answer/api/v1/user/password', params);\n};\n\nexport const resetPassword = (params: Type.PasswordResetReq) => {\n  return request.post('/answer/api/v1/user/password/reset', params);\n};\n\nexport const replacementPassword = (params: Type.PasswordReplaceReq) => {\n  return request.post('/answer/api/v1/user/password/replacement', params);\n};\n\nexport const activateAccount = (code: string) => {\n  return request.post(`/answer/api/v1/user/email/verification`, { code });\n};\n\nexport const checkImgCode = (k: Type.CaptchaKey) => {\n  const apiUrl = `/answer/api/v1/user/action/record`;\n  return request.get<Type.ImgCodeRes>(apiUrl, {\n    params: {\n      action: k,\n    },\n  });\n};\n\nexport const setNotice = (params: Type.SetNoticeReq) => {\n  return request.post('/answer/api/v1/user/notice/set', params);\n};\n\nexport const saveQuestion = (params: Type.QuestionParams) => {\n  return request.post('/answer/api/v1/question', params);\n};\n\nexport const questionDetail = (id: string) => {\n  return request.get<Type.QuestionDetailRes>(\n    `/answer/api/v1/question/info?id=${id}`,\n    { allow404: true },\n  );\n};\n\nexport const useQuestionLink = (params: {\n  question_id: string;\n  page: number;\n  page_size: number;\n  order?: string;\n}) => {\n  const apiUrl = `/answer/api/v1/question/link?${qs.stringify(params)}`;\n  const { data, error } = useSWR<Type.ListResult, Error>(\n    [apiUrl, params],\n    request.instance.get,\n  );\n  return {\n    data,\n    isLoading: !data && !error,\n    error,\n  };\n};\n\nexport const getAnswers = (params: Type.AnswersReq) => {\n  const apiUrl = `/answer/api/v1/answer/page?${qs.stringify(params)}`;\n  return request.get<Type.ListResult<Type.AnswerItem>>(apiUrl);\n};\n\nexport const postAnswer = (params: Type.PostAnswerReq) => {\n  return request.post('/answer/api/v1/answer', params);\n};\n\nexport const bookmark = (params: {\n  group_id: string;\n  object_id: string;\n  bookmark: boolean;\n}) => {\n  return request.post('/answer/api/v1/collection/switch', params);\n};\n\nexport const postVote = (\n  params: { object_id: string; is_cancel: boolean } & Type.ImgCodeReq,\n  type: 'down' | 'up',\n) => {\n  return request.post(`/answer/api/v1/vote/${type}`, params);\n};\n\nexport const following = (params: {\n  object_id: string;\n  is_cancel: boolean;\n}) => {\n  return request.post<{ follows: number; is_followed: boolean }>(\n    '/answer/api/v1/follow',\n    params,\n  );\n};\n\nexport const acceptanceAnswer = (params: {\n  answer_id?: string;\n  question_id: string;\n}) => {\n  return request.post('/answer/api/v1/answer/acceptance', params);\n};\n\nexport const reportList = ({\n  type,\n  action,\n  isBackend = false,\n}: Type.ReportParams & { isBackend }) => {\n  let api = '/answer/api/v1/reasons';\n  if (isBackend) {\n    api = '/answer/admin/api/reasons';\n  }\n  return request.get(`${api}?object_type=${type}&action=${action}`);\n};\n\nexport const postReport = (\n  params: {\n    source: Type.ReportType;\n    content: string;\n    object_id: string;\n    report_type: number;\n  } & Type.ImgCodeReq,\n) => {\n  return request.post('/answer/api/v1/report', params);\n};\n\nexport const deleteQuestion = (params: {\n  id: string;\n  captcha_code?: string;\n  captcha_id?: string;\n}) => {\n  return request.delete('/answer/api/v1/question', params);\n};\n\nexport const deleteAnswer = (params: {\n  id: string;\n  captcha_code?: string;\n  captcha_id?: string;\n}) => {\n  return request.delete('/answer/api/v1/answer', params);\n};\n\nexport const closeQuestion = (params: {\n  id: string;\n  close_msg?: string;\n  close_type: number;\n}) => {\n  return request.put('/answer/api/v1/question/status', params);\n};\n\nexport const changeEmail = (params: { e_mail: string; pass?: string }) => {\n  return request.post('/answer/api/v1/user/email/change/code', params);\n};\n\nexport const changeEmailVerify = (params: { code: string }) => {\n  return request.put('/answer/api/v1/user/email', params);\n};\n\nexport const getAppSettings = () => {\n  return request.get<Type.SiteSettings>('/answer/api/v1/siteinfo');\n};\n\nexport const reopenQuestion = (params: { question_id: string }) => {\n  return request.put('/answer/api/v1/question/reopen', params);\n};\n\nexport const unsubscribe = (code: string) => {\n  const apiUrl = '/answer/api/v1/user/notification/unsubscribe';\n  return request.put(apiUrl, { code });\n};\n\nexport const markdownToHtml = (content: string) => {\n  const apiUrl = '/answer/api/v1/post/render';\n  return request.post(apiUrl, { content });\n};\n\nexport const saveQuestionWithAnswer = (params: Type.QuestionWithAnswer) => {\n  return request.post('/answer/api/v1/question/answer', params);\n};\n\nexport const questionOperation = (params: Type.QuestionOperationReq) => {\n  return request.put('/answer/api/v1/question/operation', params);\n};\n\nexport const getPluginsStatus = () => {\n  return request.get<Type.ActivatedPlugin[]>('/answer/api/v1/plugin/status');\n};\n\nexport const deletePermanently = (type: string) => {\n  return request.delete('/answer/admin/api/delete/permanently', { type });\n};\n"
  },
  {
    "path": "ui/src/services/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport * from './admin';\nexport * from './common';\nexport * from './client';\nexport * from './install';\nexport * from './user-center';\n"
  },
  {
    "path": "ui/src/services/install/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\n\nexport const checkConfigFileExists = () => {\n  return request.post('/installation/config-file/check');\n};\n\nexport const dbCheck = (params) => {\n  return request.post('/installation/db/check', params);\n};\n\nexport const installInit = (params) => {\n  return request.post('/installation/init', params);\n};\n\nexport const installBaseInfo = (params) => {\n  return request.post('/installation/base-info', params);\n};\n\nexport const getInstallLangOptions = () => {\n  return request.get('/installation/language/options');\n};\n\nexport const getInstallLanguageConfig = (lang: string) => {\n  return request.get(`/installation/language/config?lang=${lang}`);\n};\n"
  },
  {
    "path": "ui/src/services/user-center/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport request from '@/utils/request';\n\nexport interface UcAgentControl {\n  name: string;\n  label: string;\n  url: string;\n}\nexport interface UcAgent {\n  enabled: boolean;\n  agent_info: {\n    name: string;\n    icon: string;\n    url: string;\n    display_name: string;\n    login_redirect_url: string;\n    sign_up_redirect_url: string;\n    control_center: UcAgentControl[];\n    enabled_original_user_system: boolean;\n  };\n}\n\nexport interface UcSettingAgent {\n  enabled: boolean;\n  redirect_url: string;\n}\nexport interface UcSettings {\n  profile_setting_agent: UcSettingAgent;\n  account_setting_agent: UcSettingAgent;\n}\n\nexport interface UcBrandingEntry {\n  icon: string;\n  name: string;\n  label: string;\n  url: string;\n}\nexport interface UcBranding {\n  enabled: boolean;\n  personal_branding: UcBrandingEntry[];\n}\n\nexport interface AdminUcAgent {\n  allow_create_user: boolean;\n  allow_update_user_status: boolean;\n  allow_update_user_password: boolean;\n  allow_update_user_role: boolean;\n}\n\nexport const getUcAgent = () => {\n  const apiUrl = `/answer/api/v1/user-center/agent`;\n  return request.get<UcAgent>(apiUrl);\n};\nexport const getAdminUcAgent = () => {\n  const apiUrl = `/answer/admin/api/user-center/agent`;\n  return request.get<AdminUcAgent>(apiUrl);\n};\nexport const getUcSettings = () => {\n  const apiUrl = `/answer/api/v1/user-center/user/settings`;\n  return request.get<UcSettings>(apiUrl);\n};\n\nexport const getUcBranding = (username: string) => {\n  const apiUrl = `/answer/api/v1/user-center/personal/branding?username=${username}`;\n  return request.get<UcBranding>(apiUrl);\n};\n"
  },
  {
    "path": "ui/src/stores/aiControl.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ninterface AiControlStore {\n  ai_enabled: boolean;\n  update: (params: { ai_enabled: boolean }) => void;\n  reset: () => void;\n}\n\nconst aiControlStore = create<AiControlStore>((set) => ({\n  ai_enabled: false,\n  update: (params: { ai_enabled: boolean }) =>\n    set((state) => {\n      return {\n        ...state,\n        ...params,\n      };\n    }),\n  reset: () => set({ ai_enabled: false }),\n}));\n\nexport default aiControlStore;\n"
  },
  {
    "path": "ui/src/stores/branding.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingBranding } from '@/common/interface';\n\ninterface IType {\n  branding: AdminSettingBranding;\n  update: (params: AdminSettingBranding) => void;\n}\n\nconst brandingSetting = create<IType>((set) => ({\n  branding: {\n    logo: '',\n    square_icon: '',\n    mobile_logo: '',\n    favicon: '',\n  },\n  update: (params) =>\n    set(() => {\n      return {\n        branding: params,\n      };\n    }),\n}));\n\nexport default brandingSetting;\n"
  },
  {
    "path": "ui/src/stores/commentReply.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ninterface CommentReplyType {\n  id: string | number;\n  update: (id) => void;\n}\n\nconst Index = create<CommentReplyType>((set) => ({\n  id: '',\n  update: (id) => {\n    set(() => {\n      return { id };\n    });\n  },\n}));\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/stores/customize.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ninterface IType {\n  custom_css: string;\n  custom_head: string;\n  custom_header: string;\n  custom_footer: string;\n  custom_sidebar: string;\n  update: (params: {\n    custom_css?: string;\n    custom_head?: string;\n    custom_header?: string;\n    custom_footer?: string;\n    custom_sidebar?: string;\n  }) => void;\n}\n\nconst loginSetting = create<IType>((set) => ({\n  custom_css: '',\n  custom_head: '',\n  custom_header: '',\n  custom_footer: '',\n  custom_sidebar: '',\n  update: (params) =>\n    set((state) => {\n      return {\n        ...state,\n        ...params,\n      };\n    }),\n}));\n\nexport default loginSetting;\n"
  },
  {
    "path": "ui/src/stores/errorCode.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ntype codeType = '403' | '404' | '50X' | '';\n\ninterface ErrorCodeType {\n  code: codeType;\n  msg: string;\n  update: (code: codeType, msg?: string) => void;\n  reset: () => void;\n}\n\nconst Index = create<ErrorCodeType>((set) => ({\n  code: '',\n  msg: '',\n  update: (code: codeType, msg: string = '') => {\n    set(() => {\n      return { code, msg };\n    });\n  },\n  reset: () => {\n    set(() => {\n      return { code: '', msg: '' };\n    });\n  },\n}));\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/stores/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport loginSettingStore from './loginSetting';\nimport seoSettingStore from './seoSetting';\nimport userCenterStore from './userCenter';\nimport toastStore from './toast';\nimport loggedUserInfoStore from './loggedUserInfo';\nimport siteInfoStore from './siteInfo';\nimport interfaceStore from './interface';\nimport brandingStore from './branding';\nimport pageTagStore from './pageTags';\nimport customizeStore from './customize';\nimport themeSettingStore from './themeSetting';\nimport writeSettingStore from './writeSetting';\nimport loginToContinueStore from './loginToContinue';\nimport errorCodeStore from './errorCode';\nimport sideNavStore from './sideNav';\nimport commentReplyStore from './commentReply';\nimport siteSecurityStore from './siteSecurity';\nimport aiControlStore from './aiControl';\n\nexport {\n  toastStore,\n  loggedUserInfoStore,\n  siteInfoStore,\n  interfaceStore,\n  brandingStore,\n  pageTagStore,\n  loginSettingStore,\n  customizeStore,\n  themeSettingStore,\n  seoSettingStore,\n  loginToContinueStore,\n  errorCodeStore,\n  userCenterStore,\n  sideNavStore,\n  commentReplyStore,\n  writeSettingStore,\n  siteSecurityStore,\n  aiControlStore,\n};\n"
  },
  {
    "path": "ui/src/stores/interface.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingsInterface } from '@/common/interface';\nimport { DEFAULT_LANG } from '@/common/constants';\n\ninterface InterfaceType {\n  interface: AdminSettingsInterface;\n  update: (params: AdminSettingsInterface) => void;\n}\n\nconst interfaceSetting = create<InterfaceType>((set) => ({\n  interface: {\n    language: DEFAULT_LANG,\n    time_zone: '',\n    default_avatar: 'system',\n    gravatar_base_url: '',\n  },\n  update: (params) =>\n    set((state) => {\n      return {\n        interface: { ...state.interface, ...params },\n      };\n    }),\n}));\n\nexport default interfaceSetting;\n"
  },
  {
    "path": "ui/src/stores/loggedUserInfo.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport type { UserInfoRes } from '@/common/interface';\nimport Storage from '@/utils/storage';\nimport { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';\n\ninterface UserInfoStore {\n  user: UserInfoRes;\n  update: (params: UserInfoRes) => void;\n  clear: (removeToken?: boolean) => void;\n}\n\nconst initUser: UserInfoRes = {\n  access_token: '',\n  username: '',\n  avatar: '',\n  rank: 0,\n  bio: '',\n  bio_html: '',\n  display_name: '',\n  location: '',\n  website: '',\n  status: 'normal',\n  mail_status: 1,\n  language: 'Default',\n  color_scheme: 'default',\n  is_admin: false,\n  have_password: true,\n  role_id: 1,\n};\n\nconst loggedUserInfo = create<UserInfoStore>((set) => ({\n  user: initUser,\n  update: (params) => {\n    if (typeof params !== 'object' || !params) {\n      return;\n    }\n    if (!params?.language) {\n      params.language = 'Default';\n    }\n    if (!params?.color_scheme) {\n      params.color_scheme = 'default';\n    }\n    set(() => {\n      Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token);\n      return { user: params };\n    });\n  },\n  clear: (removeToken = true) =>\n    set(() => {\n      if (removeToken) {\n        Storage.remove(LOGGED_TOKEN_STORAGE_KEY);\n      }\n      return { user: initUser };\n    }),\n}));\n\nexport default loggedUserInfo;\n"
  },
  {
    "path": "ui/src/stores/loginSetting.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingsLogin } from '@/common/interface';\n\ninterface IType {\n  login: AdminSettingsLogin;\n  update: (params: AdminSettingsLogin) => void;\n}\n\nconst loginSetting = create<IType>((set) => ({\n  login: {\n    allow_new_registrations: true,\n    allow_email_registrations: true,\n    allow_email_domains: [],\n    allow_password_login: true,\n  },\n  update: (params) =>\n    set(() => {\n      return {\n        login: params,\n      };\n    }),\n}));\n\nexport default loginSetting;\n"
  },
  {
    "path": "ui/src/stores/loginToContinue.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ninterface IProps {\n  show: boolean;\n  update: (params: { show: boolean }) => void;\n}\n\nconst loginToContinueStore = create<IProps>((set) => ({\n  show: false,\n  update: (params) =>\n    set({\n      ...params,\n    }),\n}));\n\nexport default loginToContinueStore;\n"
  },
  {
    "path": "ui/src/stores/pageTags.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { HelmetBase, HelmetUpdate } from '@/common/interface';\n\nimport siteInfoStore from './siteInfo';\n\ninterface HelmetStore {\n  items: HelmetBase;\n  update: (params: HelmetUpdate) => void;\n}\n\nconst makePageTitle = (title = '', subtitle = '') => {\n  const { siteInfo } = siteInfoStore.getState();\n  if (!subtitle) {\n    subtitle = `${siteInfo.name}`;\n  }\n  let pageTitle = subtitle;\n  if (title && title !== subtitle) {\n    pageTitle = `${title}${subtitle ? ` - ${subtitle}` : ''}`;\n  }\n  return pageTitle;\n};\n\nconst pageTags = create<HelmetStore>((set) => ({\n  items: {\n    pageTitle: '',\n    description: '',\n    keywords: '',\n  },\n  update: (params) => {\n    const o: HelmetBase = {};\n    if (params.title || params.subtitle) {\n      o.pageTitle = makePageTitle(params.title, params.subtitle);\n    }\n    o.description =\n      params.description ||\n      siteInfoStore.getState().siteInfo?.description ||\n      '';\n    o.keywords = params.keywords || '';\n\n    set({\n      items: o,\n    });\n  },\n}));\n\nexport default pageTags;\n"
  },
  {
    "path": "ui/src/stores/seoSetting.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingsSeo } from '@/common/interface';\n\ninterface IProps {\n  seo: AdminSettingsSeo;\n  update: (params: AdminSettingsSeo) => void;\n}\n\nconst Index = create<IProps>((set) => ({\n  seo: {\n    robots: '',\n    permalink: 1,\n  },\n  update: (params) =>\n    set((state) => {\n      const o = { ...state.seo, ...params };\n      // @ts-ignore\n      if (!/[1234]/.test(o.permalink)) {\n        o.permalink = 1;\n      }\n      return {\n        seo: o,\n      };\n    }),\n}));\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/stores/sideNav.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ntype reviewData = {\n  can_revision: boolean;\n  revision: number;\n};\n\ninterface ErrorCodeType {\n  can_revision: boolean;\n  revision: number;\n  updateReview: (params: reviewData) => void;\n}\n\nconst Index = create<ErrorCodeType>((set) => ({\n  can_revision: false,\n  revision: 0,\n  updateReview: (params: reviewData) => {\n    set(() => {\n      return { ...params };\n    });\n  },\n}));\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/stores/siteInfo.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingsGeneral, AdminSettingsUsers } from '@/common/interface';\nimport { DEFAULT_SITE_NAME } from '@/common/constants';\n\ninterface SiteInfoType {\n  siteInfo: AdminSettingsGeneral;\n  version: string;\n  revision: string;\n  update: (params: AdminSettingsGeneral) => void;\n  updateVersion: (ver: string, revision: string) => void;\n  users: AdminSettingsUsers;\n  updateUsers: (users: SiteInfoType['users']) => void;\n}\n\nconst defaultUsersConf: AdminSettingsUsers = {\n  allow_update_avatar: false,\n  allow_update_bio: false,\n  allow_update_display_name: false,\n  allow_update_location: false,\n  allow_update_username: false,\n  allow_update_website: false,\n  default_avatar: 'system',\n  gravatar_base_url: '',\n};\n\nconst siteInfo = create<SiteInfoType>((set) => ({\n  siteInfo: {\n    name: DEFAULT_SITE_NAME,\n    description: '',\n    short_description: '',\n    site_url: '',\n    contact_email: '',\n    permalink: 1,\n  },\n  users: defaultUsersConf,\n  version: '',\n  revision: '',\n  update: (params) =>\n    set((_) => {\n      const o = { ..._.siteInfo, ...params };\n      if (!o.name) {\n        o.name = DEFAULT_SITE_NAME;\n      }\n      return {\n        siteInfo: o,\n      };\n    }),\n  updateVersion: (ver, revision) => {\n    set(() => {\n      return { version: ver, revision };\n    });\n  },\n  updateUsers: (users) => {\n    set(() => {\n      users ||= defaultUsersConf;\n      return { users };\n    });\n  },\n}));\n\nexport default siteInfo;\n"
  },
  {
    "path": "ui/src/stores/siteSecurity.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ninterface SecurityStore {\n  login_required: boolean;\n  check_update: boolean;\n  external_content_display: string;\n  update: (params: {\n    external_content_display: string;\n    check_update: boolean;\n    login_required: boolean;\n  }) => void;\n}\n\nconst siteSecurityStore = create<SecurityStore>((set) => ({\n  login_required: false,\n  check_update: true,\n  external_content_display: 'always_display',\n  update: (params) =>\n    set((state) => {\n      return {\n        ...state,\n        ...params,\n      };\n    }),\n}));\n\nexport default siteSecurityStore;\n"
  },
  {
    "path": "ui/src/stores/themeSetting.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport { AdminSettingsTheme } from '@/common/interface';\nimport { DEFAULT_THEME_COLOR } from '@/common/constants';\n\ninterface IType {\n  theme: AdminSettingsTheme['theme'];\n  theme_config: AdminSettingsTheme['theme_config'];\n  theme_options: AdminSettingsTheme['theme_options'];\n  color_scheme: AdminSettingsTheme['color_scheme'];\n  layout: AdminSettingsTheme['layout'];\n  update: (params: AdminSettingsTheme) => void;\n}\n\nconst store = create<IType>((set) => ({\n  theme: 'default',\n  color_scheme: 'system',\n  theme_options: [{ label: 'Default', value: 'default' }],\n  theme_config: {\n    default: {\n      navbar_style: DEFAULT_THEME_COLOR,\n      primary_color: DEFAULT_THEME_COLOR,\n    },\n  },\n  layout: 'full',\n  update: (params) =>\n    set((state) => {\n      // Compatibility default value is colored or light before v1.5.1\n      if (!params.theme_config.default.navbar_style.startsWith('#')) {\n        params.theme_config.default.navbar_style = DEFAULT_THEME_COLOR;\n      }\n      return {\n        ...state,\n        ...params,\n      };\n    }),\n}));\n\nexport default store;\n"
  },
  {
    "path": "ui/src/stores/toast.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\ntype Variant = 'warning' | 'success' | 'danger';\ninterface ToastStore {\n  msg: string;\n  variant: Variant;\n  show: (params: { msg: string; variant?: Variant }) => void;\n  clear: () => void;\n}\n\nconst toastStore = create<ToastStore>((set) => ({\n  msg: '',\n  variant: 'warning',\n  show: (params) =>\n    set((state) => {\n      return {\n        ...state,\n        ...params,\n      };\n    }),\n  clear: () => set({ msg: '' }),\n}));\n\nexport default toastStore;\n"
  },
  {
    "path": "ui/src/stores/userCenter.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport type { UcAgent } from '@/services/user-center';\n\ninterface UserCenterStore {\n  agent?: UcAgent;\n  update: (uca: UcAgent) => void;\n}\n\nconst store = create<UserCenterStore>((set) => ({\n  agent: undefined,\n  update: (uca: UcAgent) => {\n    if (uca) {\n      set({\n        agent: uca,\n      });\n    }\n  },\n}));\n\nexport default store;\n"
  },
  {
    "path": "ui/src/stores/writeSetting.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { create } from 'zustand';\n\nimport {\n  AdminSettingsWrite,\n  AdminQuestionSetting,\n  AdminTagsSetting,\n} from '@/common/interface';\n\ninterface IProps {\n  write: AdminSettingsWrite & AdminQuestionSetting & AdminTagsSetting;\n  update: (\n    params: AdminSettingsWrite | AdminQuestionSetting | AdminTagsSetting,\n  ) => void;\n}\n\nconst Index = create<IProps>((set) => ({\n  write: {\n    restrict_answer: true,\n    min_tags: 1,\n    min_content: 6,\n    recommend_tags: [],\n    required_tag: false,\n    reserved_tags: [],\n    max_image_size: 4,\n    max_attachment_size: 8,\n    max_image_megapixel: 40,\n    authorized_image_extensions: [],\n    authorized_attachment_extensions: [],\n  },\n  update: (params) =>\n    set((state) => {\n      const o = { ...state.write, ...params };\n      return {\n        write: o,\n      };\n    }),\n}));\n\nexport default Index;\n"
  },
  {
    "path": "ui/src/utils/animateGift.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Progress from './progress';\n\nexport default class Confetti {\n  private parent: HTMLElement;\n\n  private canvas: HTMLCanvasElement;\n\n  private ctx;\n\n  private width: number;\n\n  private height: number;\n\n  private length: number;\n\n  private yRange: number;\n\n  private progress;\n\n  private rotationRange;\n\n  private speedRange;\n\n  private sprites;\n\n  constructor(param) {\n    this.parent = param.elm || document.body;\n    this.canvas = document.createElement('canvas');\n    this.ctx = this.canvas.getContext('2d');\n    this.width = param.width || this.parent.offsetWidth;\n    this.height = param.height || this.parent.offsetHeight;\n    this.length = param.length || Confetti.CONST.PAPER_LENGTH;\n    this.yRange = param.yRange || this.height * 2;\n    this.progress = new Progress({\n      duration: param.duration,\n      isLoop: true,\n      timestamp: null,\n      delta: 0,\n    });\n    this.rotationRange =\n      typeof param.rotationLength === 'number' ? param.rotationRange : 10;\n    this.speedRange =\n      typeof param.speedRange === 'number' ? param.speedRange : 10;\n    this.sprites = [];\n\n    this.canvas.style.cssText = [\n      'display: block',\n      'position: absolute',\n      'top: 0',\n      'left: 0',\n      'pointer-events: none',\n    ].join(';');\n\n    this.render = this.render.bind(this);\n\n    this.build();\n\n    this.parent.appendChild(this.canvas);\n    this.progress.start(performance.now());\n\n    requestAnimationFrame(this.render);\n  }\n\n  static get CONST() {\n    return {\n      SPRITE_WIDTH: 9,\n      SPRITE_HEIGHT: 16,\n      PAPER_LENGTH: 100,\n      DURATION: 8000,\n      ROTATION_RATE: 50,\n      COLORS: [\n        '#EF5350',\n        '#EC407A',\n        '#AB47BC',\n        '#7E57C2',\n        '#5C6BC0',\n        '#42A5F5',\n        '#29B6F6',\n        '#26C6DA',\n        '#26A69A',\n        '#66BB6A',\n        '#9CCC65',\n        '#D4E157',\n        '#FFEE58',\n        '#FFCA28',\n        '#FFA726',\n        '#FF7043',\n        '#8D6E63',\n        '#BDBDBD',\n        '#78909C',\n      ],\n    };\n  }\n\n  build() {\n    for (let i = 0; i < this.length; i += 1) {\n      const canvas = document.createElement('canvas') as HTMLCanvasElement & {\n        position: { initX: number; initY: number };\n        rotation;\n        speed;\n      };\n      const ctx = canvas.getContext('2d');\n\n      canvas.width = Confetti.CONST.SPRITE_WIDTH;\n      canvas.height = Confetti.CONST.SPRITE_HEIGHT;\n\n      canvas.position = {\n        initX: Math.random() * this.width,\n        initY: -canvas.height - Math.random() * this.yRange,\n      };\n\n      canvas.rotation =\n        this.rotationRange / 2 - Math.random() * this.rotationRange;\n      canvas.speed =\n        this.speedRange / 2 + Math.random() * (this.speedRange / 2);\n\n      if (ctx) {\n        ctx.save();\n        ctx.fillStyle =\n          Confetti.CONST.COLORS[\n            Math.floor(Math.random() * Confetti.CONST.COLORS.length)\n          ];\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n        ctx.restore();\n      }\n\n      this.sprites.push(canvas);\n    }\n  }\n\n  render(now) {\n    const progress = this.progress.tick(now);\n\n    this.canvas.width = this.width;\n    this.canvas.height = this.height;\n\n    for (let i = 0; i < this.length; i += 1) {\n      this.ctx.save();\n      this.ctx.translate(\n        this.sprites[i].position.initX +\n          this.sprites[i].rotation * Confetti.CONST.ROTATION_RATE * progress,\n        this.sprites[i].position.initY + progress * (this.height + this.yRange),\n      );\n      this.ctx.rotate(this.sprites[i].rotation);\n      this.ctx.drawImage(\n        this.sprites[i],\n        (-Confetti.CONST.SPRITE_WIDTH *\n          Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed))) /\n          2,\n        -Confetti.CONST.SPRITE_HEIGHT / 2,\n        Confetti.CONST.SPRITE_WIDTH *\n          Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed)),\n        Confetti.CONST.SPRITE_HEIGHT,\n      );\n      this.ctx.restore();\n    }\n\n    requestAnimationFrame(this.render);\n  }\n\n  destroy() {\n    if (this.parent.contains(this.canvas)) {\n      this.parent.removeChild(this.canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/utils/color.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport Color from 'color';\n\n/**\n * Bootstrap Color Weight:\n * $blue-100: tint-color($blue, 80%) !default;\n * $blue-200: tint-color($blue, 60%) !default;\n * $blue-300: tint-color($blue, 40%) !default;\n * $blue-400: tint-color($blue, 20%) !default;\n * $blue-500: $blue !default;\n * $blue-600: shade-color($blue, 20%) !default;\n * $blue-700: shade-color($blue, 40%) !default;\n * $blue-800: shade-color($blue, 60%) !default;\n * $blue-900: shade-color($blue, 80%) !default;\n */\n\n/**\n *  The `weight` parameter in `Color`:\n *    1. Must use decimals rather than percentages. eg: color.mix(Color(\"blue\"), 0.6)\n *    2. The value is the difference between `1 - $weight` in `bootstrap`.\n *      eg: color.mix(Color(\"blue\"), 0.6) === shade-color($blue, 40%) !default\n */\n\nconst WHITE = Color('#fff');\nconst BLACK = Color('#000');\n\nexport const mixColour = (baseColor, opColor, weight) => {\n  return Color(baseColor).mix(Color(opColor), weight);\n};\n\nexport const tintColor = (color, weight) => {\n  return mixColour(WHITE, color, weight);\n};\n\nexport const shadeColor = (color, weight) => {\n  return mixColour(BLACK, color, weight);\n};\n\nexport const shiftColor = (color, weight) => {\n  if (weight > 0) {\n    return shadeColor(color, weight);\n  }\n  return tintColor(color, -weight);\n};\n\nexport const isLight = (color) => {\n  return Color(color).isLight();\n};\n"
  },
  {
    "path": "ui/src/utils/common.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport i18next from 'i18next';\n\nimport pattern from '@/common/pattern';\nimport { USER_AGENT_NAMES } from '@/common/constants';\nimport type * as Type from '@/common/interface';\n\nconst Diff = require('diff');\n\nfunction thousandthDivision(num) {\n  const reg = /\\d{1,3}(?=(\\d{3})+$)/g;\n  return `${num}`.replace(reg, '$&,');\n}\n\nfunction formatCount($num: number): string {\n  let res = String($num);\n  if (!Number.isFinite($num)) {\n    res = '0';\n  } else if ($num < 10000) {\n    res = thousandthDivision($num);\n  } else if ($num < 1000000) {\n    res = `${Math.round($num / 100) / 10}k`;\n  } else if ($num >= 1000000) {\n    res = `${Math.round($num / 100000) / 10}m`;\n  }\n  return res;\n}\n\nfunction scrollToElementTop(element) {\n  if (!element) {\n    return;\n  }\n  const offset = 120;\n  const bodyRect = document.body.getBoundingClientRect().top;\n  const elementRect = element.getBoundingClientRect().top;\n  const elementPosition = elementRect - bodyRect;\n  const offsetPosition = elementPosition - offset;\n\n  window.scrollTo({\n    top: offsetPosition,\n    behavior: 'instant' as ScrollBehavior,\n  });\n}\n\nfunction scrollElementIntoView(element) {\n  if (!element) {\n    return;\n  }\n  element.scrollIntoView({\n    behavior: 'smooth',\n    block: 'center',\n    inline: 'center',\n  });\n}\n\nconst scrollToDocTop = () => {\n  setTimeout(() => {\n    window.scrollTo({\n      top: 0,\n      left: 0,\n      behavior: 'instant' as ScrollBehavior,\n    });\n  });\n};\n\nconst bgFadeOut = (el) => {\n  if (el && !el.classList.contains('bg-fade-out')) {\n    el.classList.add('bg-fade-out');\n    setTimeout(() => {\n      el.classList.remove('bg-fade-out');\n    }, 3200);\n  }\n};\n\n/**\n * Extract user info from markdown\n * @param markdown string\n * @returns Array<{displayName: string, userName: string}>\n */\nfunction matchedUsers(markdown) {\n  const globalReg = /\\B@([\\w|]+)/g;\n  const reg = /\\B@([\\w\\\\_\\\\.]+)/;\n\n  const users = markdown.match(globalReg);\n  if (!users) {\n    return [];\n  }\n  return users.map((user) => {\n    const matched = user.match(reg);\n    return {\n      userName: matched[1],\n    };\n  });\n}\n\n/**\n * Identify user information from markdown\n * @param markdown string\n * @returns string\n */\nfunction parseUserInfo(markdown) {\n  const globalReg = /\\B@([\\w\\\\_\\\\.\\\\-]+)/g;\n  return markdown.replace(globalReg, '[@$1](/users/$1)');\n}\n\nfunction parseEditMentionUser(markdown) {\n  const globalReg = /\\[@([^\\]]+)\\]\\([^)]+\\)/g;\n  return markdown.replace(globalReg, '@$1');\n}\n\nfunction formatUptime(value) {\n  const t = i18next.t.bind(i18next);\n  const second = parseInt(value, 10);\n\n  if (second > 60 * 60 && second < 60 * 60 * 24) {\n    const hour = second / 3600;\n    return `${Math.floor(hour)} ${\n      hour > 1 ? t('dates.hours') : t('dates.hour')\n    }`;\n  }\n  if (second > 60 * 60 * 24) {\n    const day = second / 3600 / 24;\n    return `${Math.floor(day)} ${day > 1 ? t('dates.days') : t('dates.day')}`;\n  }\n\n  return `< 1 ${t('dates.hour')}`;\n}\n\nfunction escapeRemove(str: string) {\n  if (!str || typeof str !== 'string') return str;\n  let temp: HTMLDivElement | null = document.createElement('div');\n  temp.innerHTML = str;\n  const output = temp?.innerText || temp.textContent;\n  temp = null;\n  return output;\n}\n\nfunction handleFormError(\n  error: { list: Type.FieldError[] },\n  data: any,\n  keymap?: Array<{ from: string; to: string }>,\n) {\n  if (error.list?.length > 0) {\n    error.list.forEach((item) => {\n      if (keymap?.length) {\n        const key = keymap.find((k) => k.from === item.error_field);\n        if (key) {\n          item.error_field = key.to;\n        }\n      }\n      const errorFieldObject = data[item.error_field];\n      if (errorFieldObject) {\n        errorFieldObject.isInvalid = true;\n        errorFieldObject.errorMsg = item.error_msg;\n      }\n    });\n  }\n  return data;\n}\n\nfunction escapeHtml(str: string) {\n  const tagsToReplace = {\n    '&': '&amp;',\n    '<': '&lt;',\n    '>': '&gt;',\n    '\"': '&quot;',\n    \"'\": '&#39;',\n    '`': '&#96;',\n  };\n  return str.replace(/[&<>\"'`]/g, (tag) => tagsToReplace[tag] || tag);\n}\n\nfunction formatDiffPart(part: any, className: string): string {\n  if (part.value.replace(/\\n/g, '').length <= 0) {\n    if (part.value.match(/\\n/g)?.length > 1) {\n      const value = part.value.replace(/\\n/, '');\n      return `<span class=\"${className}\"> </span><div><span class=\"${className}\">${value.replace(\n        /\\n/g,\n        ' ',\n      )}</span></div>`;\n    }\n    return `<div><span class=\"${className}\">${part.value.replace(\n      /\\n/g,\n      ' ',\n    )}</span></div>`;\n  }\n  return `<span class=\"${className}\">${part.value}</span>`;\n}\n\nfunction diffText(newText: string, oldText?: string): string {\n  if (!newText) {\n    return '';\n  }\n\n  if (typeof oldText !== 'string') {\n    return escapeHtml(newText);\n  }\n  let result = [];\n  const diff = Diff.diffChars(escapeHtml(oldText), escapeHtml(newText));\n  result = diff.map((part) => {\n    if (part.added) {\n      return formatDiffPart(part, 'review-text-add');\n    }\n    if (part.removed) {\n      return formatDiffPart(part, 'review-text-delete text-decoration-none');\n    }\n\n    return part.value;\n  });\n\n  return result.join('');\n}\n\nfunction base64ToSvg(base64: string, svgClassName?: string) {\n  try {\n    // base64 to svg xml\n    const svgxml = atob(base64);\n\n    // svg add class\n    const parser = new DOMParser();\n    const doc = parser.parseFromString(svgxml, 'image/svg+xml');\n    const parseError = doc.querySelector('parsererror');\n    const svg = doc.querySelector('svg');\n    let str = '';\n    if (svg && !parseError) {\n      if (svgClassName) {\n        svg.setAttribute('class', svgClassName);\n      }\n      // svg.classList.add('me-2');\n\n      // transform svg to string\n      const serializer = new XMLSerializer();\n      str = serializer.serializeToString(doc);\n    }\n    return str;\n  } catch (error) {\n    return '';\n  }\n}\n\n// Determine whether the user is in WeChat or Enterprise WeChat or DingTalk, and return the corresponding type\n\nfunction getUaType() {\n  const ua = navigator.userAgent.toLowerCase();\n  if (pattern.uaWeCom.test(ua)) {\n    return USER_AGENT_NAMES.WeCom;\n  }\n  if (pattern.uaWeChat.test(ua)) {\n    return USER_AGENT_NAMES.WeChat;\n  }\n  if (pattern.uaDingTalk.test(ua)) {\n    return USER_AGENT_NAMES.DingTalk;\n  }\n  return null;\n}\n\nfunction changeTheme(mode: 'default' | 'light' | 'dark' | 'system') {\n  const htmlTag = document.querySelector('html') as HTMLHtmlElement;\n  if (mode === 'system') {\n    const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');\n\n    if (systemThemeQuery.matches) {\n      htmlTag.setAttribute('data-bs-theme', 'dark');\n    } else {\n      htmlTag.setAttribute('data-bs-theme', 'light');\n    }\n  } else {\n    htmlTag.setAttribute('data-bs-theme', mode);\n  }\n}\n\nfunction isDarkTheme() {\n  const htmlTag = document.querySelector('html') as HTMLHtmlElement;\n  return htmlTag.getAttribute('data-bs-theme') === 'dark';\n}\n\nfunction pageTitleType() {\n  const { pathname } = window.location;\n  if (pathname.endsWith('/articles')) {\n    return 'articles';\n  }\n  if (pathname.endsWith('/questions')) {\n    return 'questions';\n  }\n  return 'posts';\n}\n\nexport {\n  thousandthDivision,\n  formatCount,\n  scrollElementIntoView,\n  scrollToElementTop,\n  scrollToDocTop,\n  bgFadeOut,\n  matchedUsers,\n  parseUserInfo,\n  parseEditMentionUser,\n  formatUptime,\n  escapeRemove,\n  handleFormError,\n  diffText,\n  base64ToSvg,\n  getUaType,\n  changeTheme,\n  isDarkTheme,\n  pageTitleType,\n};\n"
  },
  {
    "path": "ui/src/utils/floppyNavigation.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport type { NavigateFunction } from 'react-router-dom';\n\nimport { RouteAlias, REACT_BASE_PATH } from '@/router/alias';\nimport Storage from '@/utils/storage';\nimport { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';\nimport { getLoginUrl } from '@/utils/userCenter';\n\nconst equalToCurrentHref = (target: string, base?: string) => {\n  base ||= window.location.origin;\n  const targetUrl = new URL(\n    target.startsWith(REACT_BASE_PATH) ? target : `${REACT_BASE_PATH}${target}`,\n    base,\n  );\n  return targetUrl.toString() === window.location.href;\n};\nconst matchToCurrentHref = (target: string) => {\n  target = (target || '').trim();\n  const hasBasePath = target.startsWith(REACT_BASE_PATH);\n  // Empty string or `/` can match any path\n  if (!target || target === '/') {\n    return true;\n  }\n  const { pathname, search, hash } = window.location;\n  let pathWithOutBase = pathname;\n  if (!hasBasePath) {\n    pathWithOutBase = pathWithOutBase.replace(REACT_BASE_PATH, '');\n  }\n\n  const tPart = target.split('?');\n\n  /**\n   * With the current requirements, `hash` and `search` can simply be matched\n   * Later extended to field-by-field matching if necessary\n   */\n  if (tPart[1]) {\n    const tChip = tPart[1].split('#');\n    const tSearch = tChip[0] || '';\n    const tHash = tChip[1] || '';\n    if (tHash && hash.indexOf(tHash) === -1) {\n      return false;\n    }\n    if (tSearch && search.indexOf(tSearch) === -1) {\n      return false;\n    }\n  }\n\n  /**\n   * As determination above, `tPart[0]` must be a valid string\n   */\n  let pathMatch = true;\n  const tPath = tPart[0].split('/').filter((_) => !!_);\n  const lPath = pathWithOutBase.split('/').filter((_) => !!_);\n\n  tPath.forEach((p, i) => {\n    const lp = lPath[i];\n    if (p !== lp) {\n      pathMatch = false;\n    }\n  });\n\n  return pathMatch;\n};\n\nconst storageLoginRedirect = () => {\n  const { pathname } = window.location;\n  const filterPath = pathname.replace(REACT_BASE_PATH, '');\n  if (filterPath !== RouteAlias.login && filterPath !== RouteAlias.signUp) {\n    const loc = window.location;\n    const redirectUrl = loc.href.replace(`${loc.origin}${REACT_BASE_PATH}`, '');\n    Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);\n  }\n};\n\n/**\n * Determining if an url is a full link\n */\nconst isFullLink = (url = '') => {\n  let ret = false;\n  if (/^(http:|https:|\\/\\/)/i.test(url)) {\n    ret = true;\n  }\n  return ret;\n};\n\n/**\n * Determining if a link is routable\n */\nconst isRoutableLink = (url = '') => {\n  let ret = true;\n  if (isFullLink(url)) {\n    ret = false;\n  }\n\n  return ret;\n};\n\n/**\n * only navigate if not same as current url\n */\ntype NavigateHandler = 'href' | 'replace' | NavigateFunction;\nexport interface NavigateConfig {\n  handler?: NavigateHandler;\n  options?: any;\n}\nconst navigate = (to: string | number, config: NavigateConfig = {}) => {\n  let { handler = 'href' } = config;\n  if (to && typeof to === 'string') {\n    if (equalToCurrentHref(to)) {\n      return;\n    }\n    /**\n     * 1. Blocking redirection of two login pages\n     * 2. Auto storage login redirect\n     * Note: The or judgement cannot be missing here, both jumps will be used\n     */\n    if (to === RouteAlias.login || to === getLoginUrl()) {\n      storageLoginRedirect();\n    }\n\n    if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {\n      handler = 'href';\n    }\n    if (handler === 'href' && config.options?.replace) {\n      handler = 'replace';\n    }\n    if (handler === 'href') {\n      if (\n        to.startsWith('/') &&\n        !to.startsWith('//') &&\n        !to.startsWith(REACT_BASE_PATH)\n      ) {\n        to = `${REACT_BASE_PATH}${to}`;\n      }\n      window.location.href = to;\n    } else if (handler === 'replace') {\n      if (\n        to.startsWith('/') &&\n        !to.startsWith('//') &&\n        !to.startsWith(REACT_BASE_PATH)\n      ) {\n        to = `${REACT_BASE_PATH}${to}`;\n      }\n      window.location.replace(to);\n    } else if (typeof handler === 'function') {\n      if (to === REACT_BASE_PATH) {\n        to = '/';\n      }\n\n      if (to !== REACT_BASE_PATH && to.startsWith(REACT_BASE_PATH)) {\n        to = to.replace(REACT_BASE_PATH, '');\n      }\n      handler(to, config.options);\n    }\n  }\n  if (typeof to === 'number' && typeof handler === 'function') {\n    handler(to);\n  }\n};\n\n/**\n * auto navigate to login page\n * Note: Only the internal login page is jumped here, `userAgent` login is handled on the internal login page.\n */\nconst navigateToLogin = (config?: NavigateConfig) => {\n  const loginUrl = RouteAlias.login;\n  navigate(loginUrl, config);\n};\n\n/**\n * Determine if a Link click event should be handled\n */\nconst shouldProcessLinkClick = (evt) => {\n  if (evt.defaultPrevented) {\n    return false;\n  }\n  const nodeName = evt.currentTarget?.nodeName;\n  if (nodeName?.toLowerCase() !== 'a') {\n    return false;\n  }\n  const target = evt.currentTarget?.target;\n  return (\n    evt.button === 0 &&\n    (!target || target === '_self') &&\n    !(evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.altKey)\n  );\n};\n\n/**\n * Automatic handling of click events on route links\n */\nconst handleRouteLinkClick = (evt) => {\n  if (!shouldProcessLinkClick(evt)) {\n    return;\n  }\n  const curTarget = evt.currentTarget;\n  const href = curTarget?.getAttribute('href');\n  if (!isRoutableLink(href)) {\n    evt.preventDefault();\n    navigate(href);\n  }\n};\n\nexport const floppyNavigation = {\n  navigate,\n  navigateToLogin,\n  shouldProcessLinkClick,\n  isFullLink,\n  isRoutableLink,\n  handleRouteLinkClick,\n  equalToCurrentHref,\n  matchToCurrentHref,\n  storageLoginRedirect,\n};\n"
  },
  {
    "path": "ui/src/utils/guard.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { getLoggedUserInfo, getAppSettings } from '@/services';\nimport {\n  loggedUserInfoStore,\n  siteInfoStore,\n  interfaceStore,\n  brandingStore,\n  loginSettingStore,\n  customizeStore,\n  themeSettingStore,\n  seoSettingStore,\n  loginToContinueStore,\n  pageTagStore,\n  writeSettingStore,\n  siteSecurityStore,\n  aiControlStore,\n} from '@/stores';\nimport { RouteAlias } from '@/router/alias';\nimport {\n  LOGGED_TOKEN_STORAGE_KEY,\n  REDIRECT_PATH_STORAGE_KEY,\n} from '@/common/constants';\nimport Storage from '@/utils/storage';\n\nimport { setupAppLanguage, setupAppTimeZone, setupAppTheme } from './localize';\nimport { floppyNavigation, NavigateConfig } from './floppyNavigation';\nimport { pullUcAgent, getSignUpUrl } from './userCenter';\n\ntype TLoginState = {\n  isLogged: boolean;\n  isNotActivated: boolean;\n  isActivated: boolean;\n  isNormal: boolean;\n  isAdmin: boolean;\n  isModerator: boolean;\n};\n\nexport type TGuardResult = {\n  ok: boolean;\n  redirect?: string;\n  error?: {\n    code?: number | string;\n    msg?: string;\n  };\n};\nexport type TGuardFunc = (args: {\n  loaderData?: any;\n  path?: string;\n  page?: string;\n}) => TGuardResult;\n\nexport const deriveLoginState = (): TLoginState => {\n  const ls: TLoginState = {\n    isLogged: false,\n    isNotActivated: false,\n    isActivated: false,\n    isNormal: false,\n    isAdmin: false,\n    isModerator: false,\n  };\n  const { user } = loggedUserInfoStore.getState();\n  if (user.access_token) {\n    ls.isLogged = true;\n  }\n  if (ls.isLogged && user.mail_status === 1) {\n    ls.isActivated = true;\n  }\n  if (ls.isLogged && user.mail_status === 2) {\n    ls.isNotActivated = true;\n  }\n\n  if (ls.isActivated) {\n    ls.isNormal = true;\n  }\n  if (ls.isNormal && user.role_id === 2) {\n    ls.isAdmin = true;\n  }\n  if (ls.isNormal && user.role_id === 3) {\n    ls.isModerator = true;\n  }\n  return ls;\n};\n\nexport const IGNORE_PATH_LIST = [\n  RouteAlias.login,\n  RouteAlias.signUp,\n  RouteAlias.accountRecovery,\n  RouteAlias.changeEmail,\n  RouteAlias.passwordReset,\n  RouteAlias.accountActivation,\n  RouteAlias.confirmNewEmail,\n  RouteAlias.confirmEmail,\n  RouteAlias.authLanding,\n  '/user-center/',\n];\n\nexport const isIgnoredPath = (ignoredPath?: string | string[]) => {\n  if (!ignoredPath) {\n    ignoredPath = IGNORE_PATH_LIST;\n  }\n  if (!Array.isArray(ignoredPath)) {\n    ignoredPath = [ignoredPath];\n  }\n  const matchingPath = ignoredPath.find((p) => {\n    return floppyNavigation.matchToCurrentHref(p);\n  });\n  return !!matchingPath;\n};\n\nlet pluTimestamp = 0;\nexport const pullLoggedUser = async (isInitPull = false) => {\n  /**\n   * WARN:\n   * - dedupe pull requests in this time span in 10 seconds\n   * - isInitPull:\n   *   Requests sent by the initialisation method cannot be throttled\n   *   and may cause Promise.allSettled to complete early in React development mode,\n   *   resulting in inaccurate application data.\n   */\n  //\n  if (!isInitPull && Date.now() - pluTimestamp < 1000 * 10) {\n    return;\n  }\n  pluTimestamp = Date.now();\n  const loggedUserInfo = await getLoggedUserInfo({\n    passingError: true,\n  }).catch(() => {\n    pluTimestamp = 0;\n    loggedUserInfoStore.getState().clear(false);\n  });\n  if (loggedUserInfo) {\n    loggedUserInfoStore.getState().update(loggedUserInfo);\n  }\n};\n\nexport const logged = () => {\n  const gr: TGuardResult = { ok: true };\n  const us = deriveLoginState();\n  if (!us.isLogged) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.login;\n  }\n  return gr;\n};\n\nexport const loggedRedirectHome = () => {\n  const gr: TGuardResult = { ok: true };\n  const us = deriveLoginState();\n  if (!us.isLogged) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.home;\n  }\n  return gr;\n};\n\nexport const notLogged = () => {\n  const gr: TGuardResult = { ok: true };\n  const us = deriveLoginState();\n  if (us.isLogged) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.home;\n  }\n  return gr;\n};\n\nexport const notActivated = () => {\n  const gr: TGuardResult = { ok: true };\n  const us = deriveLoginState();\n  if (us.isActivated) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.home;\n  }\n  return gr;\n};\n\nexport const activated = () => {\n  const gr = logged();\n  const us = deriveLoginState();\n  if (us.isNotActivated) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.inactive;\n  }\n  return gr;\n};\n\nexport const admin = () => {\n  const gr = logged();\n  const us = deriveLoginState();\n  if (gr.ok && !us.isAdmin) {\n    gr.ok = false;\n    gr.error = {\n      code: '403',\n      msg: '',\n    };\n    gr.redirect = '';\n  }\n  return gr;\n};\n\nexport const isAdminOrModerator = () => {\n  const gr = logged();\n  const us = deriveLoginState();\n  if (gr.ok && !us.isAdmin && !us.isModerator) {\n    gr.ok = false;\n    gr.error = {\n      code: '403',\n      msg: '',\n    };\n    gr.redirect = '';\n  }\n  return gr;\n};\n\nexport const isEditable = (args) => {\n  const loaderData = args?.loaderData || {};\n  const gr: TGuardResult = { ok: true };\n  if (loaderData.code === 400) {\n    gr.ok = false;\n    gr.error = {\n      code: '403',\n      msg: loaderData.msg,\n    };\n  }\n  return gr;\n};\n\nexport const allowNewRegistration = () => {\n  const gr: TGuardResult = { ok: true };\n  const loginSetting = loginSettingStore.getState().login;\n  if (!loginSetting.allow_new_registrations) {\n    gr.ok = false;\n    gr.redirect = RouteAlias.home;\n  }\n  return gr;\n};\n\nexport const singUpAgent = () => {\n  const gr: TGuardResult = { ok: true };\n  const signUpUrl = getSignUpUrl();\n  if (signUpUrl !== RouteAlias.signUp) {\n    gr.ok = false;\n    gr.redirect = signUpUrl;\n  }\n  return gr;\n};\n\nexport const shouldLoginRequired = () => {\n  const gr: TGuardResult = { ok: true };\n  const { login_required } = siteSecurityStore.getState();\n  if (!login_required) {\n    return gr;\n  }\n  const us = deriveLoginState();\n  if (us.isLogged) {\n    return gr;\n  }\n  if (isIgnoredPath(IGNORE_PATH_LIST)) {\n    return gr;\n  }\n  gr.ok = false;\n  gr.redirect = RouteAlias.login;\n  return gr;\n};\n\n/**\n * try user was logged and all state ok\n * @param canNavigate // if true, will navigate to login page if not logged\n */\nexport const tryNormalLogged = (canNavigate: boolean = false) => {\n  const us = deriveLoginState();\n\n  if (us.isNormal) {\n    return true;\n  }\n  // must assert logged state first and return\n  if (!us.isLogged) {\n    if (canNavigate) {\n      loginToContinueStore.getState().update({ show: true });\n    }\n    return false;\n  }\n  if (us.isNotActivated) {\n    floppyNavigation.navigate(RouteAlias.inactive);\n  }\n\n  return false;\n};\n\nexport const tryLoggedAndActivated = () => {\n  const gr: TGuardResult = { ok: true };\n  const us = deriveLoginState();\n\n  if (!us.isLogged || !us.isActivated) {\n    gr.ok = false;\n  }\n  return gr;\n};\n\n/**\n * Auto handling of page redirect logic after a successful login\n */\nexport const handleLoginRedirect = (handler?: NavigateConfig['handler']) => {\n  const redirectUrl = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;\n  Storage.remove(REDIRECT_PATH_STORAGE_KEY);\n  floppyNavigation.navigate(redirectUrl, {\n    handler,\n    options: { replace: true },\n  });\n};\n\n/**\n * Unified processing of login logic after getting `access_token`\n */\nexport const handleLoginWithToken = (\n  token: string | null,\n  handler?: NavigateConfig['handler'],\n) => {\n  if (token) {\n    Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);\n    setTimeout(() => {\n      getLoggedUserInfo().then((res) => {\n        loggedUserInfoStore.getState().update(res);\n        const userStat = deriveLoginState();\n        if (userStat.isNotActivated) {\n          floppyNavigation.navigate(RouteAlias.inactive, {\n            handler,\n            options: {\n              replace: true,\n            },\n          });\n        } else {\n          handleLoginRedirect(handler);\n        }\n      });\n    });\n  } else {\n    floppyNavigation.navigate(RouteAlias.home, {\n      handler,\n      options: {\n        replace: true,\n      },\n    });\n  }\n};\n\n/**\n * Initialize app configuration\n */\nexport const initAppSettingsStore = async () => {\n  const appSettings = await getAppSettings();\n  if (appSettings) {\n    siteInfoStore.getState().update(appSettings.general);\n    siteInfoStore\n      .getState()\n      .updateVersion(appSettings.version, appSettings.revision);\n    siteInfoStore.getState().updateUsers(appSettings.site_users);\n    interfaceStore.getState().update(appSettings.interface);\n    pageTagStore.getState().update({\n      title: appSettings.general?.name,\n      description: appSettings.general?.description,\n    });\n    brandingStore.getState().update(appSettings.branding);\n    loginSettingStore.getState().update(appSettings.login);\n    customizeStore.getState().update(appSettings.custom_css_html);\n    themeSettingStore.getState().update(appSettings.theme);\n    seoSettingStore.getState().update(appSettings.site_seo);\n    writeSettingStore.getState().update({\n      ...appSettings.site_advanced,\n      ...appSettings.site_questions,\n      ...appSettings.site_tags,\n    });\n    aiControlStore.getState().update({\n      ai_enabled: appSettings.ai_enabled,\n    });\n    siteSecurityStore.getState().update(appSettings.site_security);\n  }\n};\n\nexport const askRedirect = () => {\n  const queryString = window.location.search;\n\n  return {\n    ok: false,\n    redirect: `/questions/add${queryString}`,\n  };\n};\n\nexport const linkedRedirect = () => {\n  const queryString = window.location.search;\n  const pathname = window.location.pathname.replace('/questions', '');\n\n  return {\n    ok: false,\n    redirect: `${pathname}${queryString}`,\n  };\n};\n\nlet appInitialized = false;\nexport const setupApp = async () => {\n  /**\n   * This cannot be removed:\n   * clicking on the current navigation link will trigger a call to the routing loader,\n   * even though the page is not refreshed.\n   */\n  if (appInitialized) {\n    return;\n  }\n  /**\n   * WARN:\n   * 1. must pre init logged user info for router guard\n   * 2. must pre init app settings for app render\n   */\n  await Promise.allSettled([initAppSettingsStore(), pullLoggedUser(true)]);\n  await Promise.allSettled([pullUcAgent()]);\n  setupAppLanguage();\n  setupAppTimeZone();\n  setupAppTheme();\n  /**\n   * WARN:\n   * Initialization must be completed after all initialization actions,\n   * otherwise the problem of rendering twice in React development mode can lead to inaccurate data or flickering pages\n   */\n  appInitialized = true;\n};\n"
  },
  {
    "path": "ui/src/utils/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nexport { default as request } from './request';\nexport { default as Storage } from './storage';\nexport { floppyNavigation } from './floppyNavigation';\nexport { default as storageExpires } from './storageWithExpires';\nexport { default as SaveDraft } from './saveDraft';\n\nexport * from './common';\nexport * from './color';\nexport * as userCenter from './userCenter';\nexport * as localize from './localize';\nexport * as guard from './guard';\n"
  },
  {
    "path": "ui/src/utils/localize.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport dayjs from 'dayjs';\nimport i18next from 'i18next';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\n\nimport {\n  interfaceStore,\n  loggedUserInfoStore,\n  themeSettingStore,\n} from '@/stores';\nimport {\n  CURRENT_LANG_STORAGE_KEY,\n  DEFAULT_LANG,\n  LANG_RESOURCE_STORAGE_KEY,\n  DEFAULT_THEME,\n} from '@/common/constants';\nimport {\n  getAdminLanguageOptions,\n  getInstallLanguageConfig,\n  getLanguageConfig,\n  getLanguageOptions,\n} from '@/services';\nimport { changeTheme } from '@/utils/common';\n\nimport Storage from './storage';\n\n/**\n * localize kit for i18n\n */\nexport const loadLanguageOptions = async (forAdmin = false) => {\n  const languageOptions = forAdmin\n    ? await getAdminLanguageOptions()\n    : await getLanguageOptions();\n  if (process.env.NODE_ENV === 'development') {\n    const { default: optConf } = await import('@i18n/i18n.yaml');\n    optConf?.language_options.forEach((opt) => {\n      if (!languageOptions.find((_) => opt.value === _.value)) {\n        languageOptions.push(opt);\n      }\n    });\n  }\n  return languageOptions;\n};\n\nconst pullLanguageConf = (res) => {\n  if (window.location.pathname === '/install') {\n    return getInstallLanguageConfig(res.lng).then((langConf) => {\n      if (langConf && langConf.ui) {\n        res.resources = langConf.ui;\n        Storage.set(LANG_RESOURCE_STORAGE_KEY, res);\n      }\n    });\n  }\n\n  return getLanguageConfig().then((langConf) => {\n    if (langConf && langConf.ui) {\n      res.resources = langConf.ui;\n      Storage.set(LANG_RESOURCE_STORAGE_KEY, res);\n    }\n  });\n};\n\nconst addI18nResource = async (langName) => {\n  const res = { lng: langName, resources: undefined };\n  const storageResource = Storage.get(LANG_RESOURCE_STORAGE_KEY);\n  if (process.env.NODE_ENV === 'development') {\n    try {\n      const { default: resConf } = await import(`@i18n/${langName}.yaml`);\n      res.resources = resConf.ui;\n    } catch (ex) {\n      console.error('addI18nResource error: ', ex);\n    }\n  } else if (storageResource && storageResource.lng === res.lng) {\n    res.resources = storageResource.resources;\n    pullLanguageConf(res);\n  } else {\n    await pullLanguageConf(res);\n  }\n\n  if (res.resources) {\n    i18next.addResourceBundle(\n      res.lng,\n      'translation',\n      res.resources,\n      true,\n      true,\n    );\n  }\n};\n\nexport const getCurrentLang = () => {\n  const loggedUser = loggedUserInfoStore.getState().user;\n  const adminInterface = interfaceStore.getState().interface;\n  const fallbackLang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG;\n  let currentLang = loggedUser.language;\n  // `default` mean use language value from admin interface\n  if (/default/i.test(currentLang)) {\n    currentLang = adminInterface.language;\n  }\n  currentLang ||= fallbackLang;\n  return currentLang;\n};\n\nexport const getCurrentTheme = () => {\n  const loggedUser = loggedUserInfoStore.getState().user;\n  const adminTheme = themeSettingStore.getState().color_scheme;\n  const fallbackTheme = DEFAULT_THEME;\n  let currentTheme = loggedUser.color_scheme;\n  if (/default/i.test(currentTheme)) {\n    currentTheme = adminTheme;\n  }\n  currentTheme ||= fallbackTheme;\n  return currentTheme;\n};\n\n/**\n * localize for Day.js\n */\ndayjs.extend(utc);\ndayjs.extend(timezone);\nconst localeDayjs = (langName) => {\n  langName = langName.replace('_', '-').toLowerCase();\n  dayjs.locale(langName);\n};\n\nexport const setupAppLanguage = async () => {\n  const lang = getCurrentLang();\n  await addI18nResource(lang);\n  localeDayjs(lang);\n  i18next.changeLanguage(lang);\n};\n\nexport const setupAppTimeZone = () => {\n  const adminInterface = interfaceStore.getState().interface;\n  if (adminInterface.time_zone) {\n    dayjs.tz.setDefault(adminInterface.time_zone);\n  }\n};\n\nexport const setupAppTheme = () => {\n  const theme = getCurrentTheme();\n  changeTheme(theme);\n};\n\nexport const setupInstallLanguage = async (lang) => {\n  await addI18nResource(lang);\n  localeDayjs(lang);\n  i18next.changeLanguage(lang);\n};\n"
  },
  {
    "path": "ui/src/utils/pluginKit/index.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { RefObject } from 'react';\n\nimport builtin from '@/plugins/builtin';\nimport * as allPlugins from '@/plugins';\nimport type * as Type from '@/common/interface';\nimport { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';\nimport { getPluginsStatus } from '@/services';\nimport Storage from '@/utils/storage';\nimport request from '@/utils/request';\n\nimport { initI18nResource } from './utils';\nimport { Plugin, PluginInfo, PluginType } from './interface';\n\n/**\n * This information is to be defined for all components.\n * It may be used for feature upgrades or version compatibility processing.\n *\n * @field slug_name: Unique identity string for the plugin, usually configured in `info.yaml`\n * @field type: The type of plugin is defined and a single type of plugin can have multiple implementations.\n *              For example, a plugin of type `connector` can have a `google` implementation and a `github` implementation.\n *              `PluginRender` automatically renders the plug-in types already included in `PluginType`.\n * @field name: Plugin name, optionally configurable. Usually read from the `i18n` file\n * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file\n */\n\nclass Plugins {\n  plugins: Plugin[] = [];\n\n  registeredPlugins: Type.ActivatedPlugin[] = [];\n\n  initialization: Promise<void>;\n\n  private isInitialized = false;\n\n  private initializationError: Error | null = null;\n\n  private replacementPlugins: Map<PluginType, Plugin> = new Map();\n\n  constructor() {\n    this.initialization = this.init();\n  }\n\n  async init() {\n    if (this.isInitialized) {\n      return;\n    }\n\n    try {\n      this.registerBuiltin();\n\n      // Note: The /install stage does not allow access to the getPluginsStatus api, so an initial value needs to be given\n      const plugins =\n        (await getPluginsStatus().catch((error) => {\n          console.warn('Failed to get plugins status:', error);\n          return [];\n        })) || [];\n      this.registeredPlugins = plugins.filter((p) => p.enabled);\n      await this.registerPlugins();\n      this.isInitialized = true;\n      this.initializationError = null;\n    } catch (error) {\n      this.initializationError = error as Error;\n      console.error('Plugin initialization failed:', error);\n      throw error;\n    }\n  }\n\n  async refresh() {\n    this.plugins = [];\n    this.isInitialized = false;\n    this.initializationError = null;\n    this.initialization = this.init();\n    await this.initialization;\n  }\n\n  validate(plugin: Plugin) {\n    if (!plugin) {\n      return false;\n    }\n    const { info } = plugin;\n    const { slug_name, type } = info;\n\n    if (!slug_name) {\n      return false;\n    }\n\n    if (!type) {\n      return false;\n    }\n\n    return true;\n  }\n\n  registerBuiltin() {\n    Object.keys(builtin).forEach((key) => {\n      const plugin = builtin[key];\n      // builttin plugins are always activated\n      // Use own internal rendering logic'\n      plugin.activated = true;\n      this.register(plugin);\n    });\n  }\n\n  async registerPlugins() {\n    console.log(\n      '[PluginKit] Registered plugins from API:',\n      this.registeredPlugins.map((p) => p.slug_name),\n    );\n\n    const pluginLoaders = this.registeredPlugins\n      .map((p) => {\n        const func = allPlugins[p.slug_name];\n        if (!func) {\n          console.warn(\n            `[PluginKit] Plugin loader not found for: ${p.slug_name}`,\n          );\n        }\n        return { slug_name: p.slug_name, loader: func };\n      })\n      .filter((p) => p.loader);\n\n    console.log(\n      '[PluginKit] Found plugin loaders:',\n      pluginLoaders.map((p) => p.slug_name),\n    );\n\n    // Use Promise.allSettled to prevent one plugin failure from breaking all plugins\n    const results = await Promise.allSettled(\n      pluginLoaders.map((p) => p.loader()),\n    );\n\n    results.forEach((result, index) => {\n      if (result.status === 'fulfilled') {\n        console.log(\n          `[PluginKit] Successfully loaded plugin: ${pluginLoaders[index].slug_name}`,\n        );\n        this.register(result.value);\n      } else {\n        console.error(\n          `[PluginKit] Failed to load plugin ${pluginLoaders[index].slug_name}:`,\n          result.reason,\n        );\n      }\n    });\n  }\n\n  register(plugin: Plugin) {\n    const bool = this.validate(plugin);\n    if (!bool) {\n      return;\n    }\n\n    // Prevent duplicate registration\n    const exists = this.plugins.some(\n      (p) => p.info.slug_name === plugin.info.slug_name,\n    );\n    if (exists) {\n      console.warn(`Plugin ${plugin.info.slug_name} is already registered`);\n      return;\n    }\n\n    // Handle singleton plugins (only one per type allowed)\n    const mode = plugin.info.registrationMode || 'multiple';\n    if (mode === 'singleton') {\n      const existingPlugin = this.replacementPlugins.get(plugin.info.type);\n      if (existingPlugin) {\n        const error = new Error(\n          `[PluginKit] Plugin conflict: ` +\n            `Cannot register '${plugin.info.slug_name}' because '${existingPlugin.info.slug_name}' ` +\n            `is already registered as a singleton plugin of type '${plugin.info.type}'. ` +\n            `Only one singleton plugin per type is allowed.`,\n        );\n        console.error(error.message);\n        throw error;\n      }\n      this.replacementPlugins.set(plugin.info.type, plugin);\n    }\n\n    if (plugin.i18nConfig) {\n      initI18nResource(plugin.i18nConfig);\n    }\n    plugin.activated = true;\n    this.plugins.push(plugin);\n  }\n\n  getPlugin(slug_name: string) {\n    return this.plugins.find((p) => p.info.slug_name === slug_name);\n  }\n\n  getOnePluginHooks(slug_name: string) {\n    const plugin = this.getPlugin(slug_name);\n    return plugin?.hooks;\n  }\n\n  getPlugins() {\n    return this.plugins;\n  }\n\n  async getPluginsAsync() {\n    await this.initialization;\n    return this.plugins;\n  }\n\n  getInitializationStatus() {\n    return {\n      isInitialized: this.isInitialized,\n      error: this.initializationError,\n    };\n  }\n\n  getReplacementPlugin(type: PluginType): Plugin | null {\n    return this.replacementPlugins.get(type) || null;\n  }\n}\n\nconst plugins = new Plugins();\n\nconst getRoutePlugins = async () => {\n  await plugins.initialization;\n  return plugins\n    .getPlugins()\n    .filter((plugin) => plugin.info.type === PluginType.Route);\n};\n\nconst defaultProps = () => {\n  const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || '';\n  return {\n    key: token,\n    headers: {\n      Authorization: token,\n    },\n  };\n};\n\nconst validateRoutePlugin = async (slugName) => {\n  let registeredPlugin;\n  if (plugins.registeredPlugins.length === 0) {\n    const pluginsStatus = await getPluginsStatus();\n    registeredPlugin = pluginsStatus.find((p) => p.slug_name === slugName);\n  } else {\n    registeredPlugin = plugins.registeredPlugins.find(\n      (p) => p.slug_name === slugName,\n    );\n  }\n\n  return Boolean(registeredPlugin?.enabled);\n};\n\nconst getReplacementPlugin = async (\n  type: PluginType,\n): Promise<Plugin | null> => {\n  try {\n    await plugins.initialization;\n    return plugins.getReplacementPlugin(type);\n  } catch (error) {\n    console.error(\n      `[PluginKit] Failed to get replacement plugin of type ${type}:`,\n      error,\n    );\n    return null;\n  }\n};\n\nconst mergeRoutePlugins = async (routes) => {\n  const routePlugins = await getRoutePlugins();\n  if (routePlugins.length === 0) {\n    return routes;\n  }\n  routes.forEach((route) => {\n    if (route.page === 'pages/Layout') {\n      route.children?.forEach((child) => {\n        if (child.page === 'pages/SideNavLayout') {\n          routePlugins.forEach((plugin) => {\n            const { route: path, slug_name } = plugin.info;\n            child.children.push({\n              page: plugin.component,\n              path,\n              loader: async () => {\n                const bool = await validateRoutePlugin(slug_name);\n                return bool;\n              },\n              guard: (params) => {\n                if (params.loaderData) {\n                  return {\n                    ok: true,\n                  };\n                }\n\n                return {\n                  ok: false,\n                  error: {\n                    code: 404,\n                  },\n                };\n              },\n            });\n          });\n        }\n      });\n    }\n  });\n  return routes;\n};\n\n/**\n * Only used to enhance the capabilities of the markdown editor\n * Add RefObject type to solve the problem of dom being null in hooks\n */\nconst useRenderHtmlPlugin = (\n  element: HTMLElement | RefObject<HTMLElement> | null,\n) => {\n  plugins\n    .getPlugins()\n    .filter((plugin) => {\n      return (\n        plugin.activated &&\n        plugin.hooks?.useRender &&\n        (plugin.info.type === PluginType.Editor ||\n          plugin.info.type === PluginType.Render)\n      );\n    })\n    .forEach((plugin) => {\n      plugin.hooks?.useRender?.forEach((hook) => {\n        hook(element, request);\n      });\n    });\n};\n\n// Only for render type plugins\nconst useRenderPlugin = (\n  element: HTMLElement | RefObject<HTMLElement> | null,\n) => {\n  return plugins\n    .getPlugins()\n    .filter((plugin) => {\n      return (\n        plugin.activated &&\n        plugin.hooks?.useRender &&\n        plugin.info.type === PluginType.Render\n      );\n    })\n    .forEach((plugin) => {\n      plugin.hooks?.useRender?.forEach((hook) => {\n        hook(element, request);\n      });\n    });\n};\n\n// Only one captcha type plug-in can be enabled at the same time\nconst useCaptchaPlugin = (key: Type.CaptchaKey) => {\n  const captcha = plugins\n    .getPlugins()\n    .filter(\n      (plugin) => plugin.info.type === PluginType.Captcha && plugin.activated,\n    );\n  const pluginHooks = plugins.getOnePluginHooks(captcha[0]?.info.slug_name);\n  return pluginHooks?.useCaptcha?.({\n    captchaKey: key,\n    commonProps: defaultProps(),\n  });\n};\n\nexport type { Plugin, PluginInfo };\n\nexport {\n  useRenderHtmlPlugin,\n  mergeRoutePlugins,\n  useCaptchaPlugin,\n  useRenderPlugin,\n  getReplacementPlugin,\n  PluginType,\n};\nexport default plugins;\n"
  },
  {
    "path": "ui/src/utils/pluginKit/interface.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { NamedExoticComponent, FC, RefObject } from 'react';\n\nimport type * as Type from '@/common/interface';\nimport Request from '@/utils/request';\n\nexport enum PluginType {\n  Connector = 'connector',\n  Search = 'search',\n  Editor = 'editor',\n  EditorReplacement = 'editor_replacement',\n  Route = 'route',\n  Captcha = 'captcha',\n  Render = 'render',\n  Sidebar = 'sidebar',\n}\n\nexport interface PluginInfo {\n  slug_name: string;\n  type: PluginType;\n  name?: string;\n  description?: string;\n  route?: string;\n  registrationMode?: 'multiple' | 'singleton';\n}\n\nexport interface Plugin {\n  info: PluginInfo;\n  component: NamedExoticComponent | FC;\n  i18nConfig?;\n  hooks?: {\n    useRender?: Array<\n      (\n        element: HTMLElement | RefObject<HTMLElement> | null,\n        request?: typeof Request,\n      ) => void\n    >;\n    useCaptcha?: (props: { captchaKey: Type.CaptchaKey; commonProps: any }) => {\n      getCaptcha: () => Record<string, any>;\n      check: (t: () => void) => void;\n      handleCaptchaError: (error) => any;\n      close: () => Promise<void>;\n      resolveCaptchaReq: (data) => void;\n    };\n  };\n  activated?: boolean;\n}\n"
  },
  {
    "path": "ui/src/utils/pluginKit/utils.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport i18next from 'i18next';\n\nimport { PluginInfo, Plugin, PluginType } from './interface';\n\n/**\n * This information is to be defined for all components.\n * It may be used for feature upgrades or version compatibility processing.\n *\n * @field slug_name: Unique identity string for the plugin, usually configured in `info.yaml`\n * @field type: The type of plugin is defined and a single type of plugin can have multiple implementations.\n *              For example, a plugin of type `connector` can have a `google` implementation and a `github` implementation.\n *              `PluginRender` automatically renders the plug-in types already included in `PluginType`.\n * @field name: Plugin name, optionally configurable. Usually read from the `i18n` file\n * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file\n */\n\nconst I18N_NS = 'plugin';\ninterface I18nResource {\n  [lng: string]: {\n    plugin: {\n      [slug_name: string]: {\n        ui: any;\n      };\n    };\n  };\n}\n\nconst addResourceBundle = (resource: I18nResource) => {\n  if (resource) {\n    Object.keys(resource).forEach((lng) => {\n      const r = resource[lng];\n\n      i18next.addResourceBundle(lng, I18N_NS, r.plugin, true, true);\n    });\n  }\n};\n\nconst initI18nResource = (resource: I18nResource) => {\n  addResourceBundle(resource);\n  /**\n   * Note: In development mode,\n   * when the base i18n file is changed, `i18next` will reinitialise the updated resource file,\n   * which will cause the resource package added in the plugin to be lost\n   * and will need to be automatically re-added by listening for events\n   */\n  i18next.on('initialized', () => {\n    addResourceBundle(resource);\n  });\n};\n\nconst getTransNs = () => {\n  return I18N_NS;\n};\n\nconst getTransKeyPrefix = (info: PluginInfo) => {\n  const kp = `${info.slug_name}.ui`;\n  return kp;\n};\n\nexport type { Plugin, PluginInfo, PluginType };\n\nexport { initI18nResource, getTransNs, getTransKeyPrefix };\n"
  },
  {
    "path": "ui/src/utils/progress.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nclass Progress {\n  private timestamp;\n\n  private duration: number;\n\n  private progress: number;\n\n  private delta: number;\n\n  private isLoop: boolean;\n\n  constructor(\n    param = {\n      timestamp: null,\n      duration: 1000,\n      delta: 0,\n      isLoop: true,\n    },\n  ) {\n    this.timestamp = null;\n    this.duration = param.duration || Progress.CONST.DURATION;\n    this.progress = 0;\n    this.delta = 0;\n    this.isLoop = !!param.isLoop;\n\n    this.reset();\n  }\n\n  static get CONST() {\n    return {\n      DURATION: 1000,\n    };\n  }\n\n  reset() {\n    this.timestamp = null;\n  }\n\n  start(now) {\n    this.timestamp = now;\n  }\n\n  tick(now) {\n    if (this.timestamp) {\n      this.delta = now - this.timestamp;\n      this.progress = Math.min(this.delta / this.duration, 1);\n\n      if (this.progress >= 1 && this.isLoop) {\n        this.start(now);\n      }\n\n      return this.progress;\n    }\n    return 0;\n  }\n}\n\nexport default Progress;\n"
  },
  {
    "path": "ui/src/utils/request.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport axios, { AxiosResponse } from 'axios';\nimport type {\n  AxiosInstance,\n  AxiosRequestConfig,\n  InternalAxiosRequestConfig,\n  AxiosError,\n} from 'axios';\n\nimport { Modal } from '@/components';\nimport { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores';\nimport { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';\nimport { RouteAlias } from '@/router/alias';\nimport { getCurrentLang } from '@/utils/localize';\n\nimport Storage from './storage';\nimport { floppyNavigation } from './floppyNavigation';\nimport { isIgnoredPath, IGNORE_PATH_LIST } from './guard';\n\nconst baseConfig = {\n  baseURL:\n    process.env.NODE_ENV === 'development' ? '' : process.env.REACT_APP_API_URL,\n  timeout: 10000,\n  withCredentials: true,\n};\n\ninterface ApiConfig extends AxiosRequestConfig {\n  // Configure whether to allow takeover of 404 errors\n  allow404?: boolean;\n  ignoreError?: '403' | '50X';\n  // Configure whether to pass errors directly\n  passingError?: boolean;\n}\n\nclass Request {\n  instance: AxiosInstance;\n\n  constructor(config: AxiosRequestConfig) {\n    this.instance = axios.create(config);\n    this.instance.interceptors.request.use(\n      (requestConfig: InternalAxiosRequestConfig) => {\n        const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || '';\n        const lang = getCurrentLang();\n        requestConfig.headers.set('Authorization', token);\n        requestConfig.headers.set('Accept-Language', lang);\n        return requestConfig;\n      },\n      (err: AxiosError) => {\n        console.error('request interceptors error:', err);\n      },\n    );\n\n    this.instance.interceptors.response.use(\n      (res: AxiosResponse) => {\n        const { status, data } = res.data;\n\n        if (status === 204) {\n          // no content\n          return true;\n        }\n        return data;\n      },\n      (error) => {\n        const {\n          status,\n          data: errBody,\n          config: errConfig,\n        } = error.response || {};\n        const { data = {}, msg = '' } = errBody || {};\n\n        const errorObject: {\n          code: any;\n          msg: string;\n          data: any;\n          // Currently only used for form errors\n          isError?: boolean;\n          // Currently only used for form errors\n          list?: any[];\n        } = {\n          code: status,\n          msg,\n          data,\n        };\n\n        if (status === 400) {\n          if (data?.err_type && errConfig?.passingError) {\n            return Promise.reject(errorObject);\n          }\n          if (data?.err_type) {\n            if (data.err_type === 'toast') {\n              // toast error message\n              toastStore.getState().show({\n                msg,\n                variant: 'danger',\n              });\n            }\n\n            if (data.err_type === 'alert') {\n              return Promise.reject({\n                msg,\n                ...data,\n              });\n            }\n\n            if (data.err_type === 'modal') {\n              // modal error message\n              Modal.confirm({\n                content: msg,\n              });\n            }\n\n            return Promise.reject(false);\n          }\n\n          if (data instanceof Array && data.length > 0) {\n            // handle form error\n            errorObject.isError = true;\n            errorObject.list = data;\n            return Promise.reject(errorObject);\n          }\n\n          if (!data || Object.keys(data).length <= 0) {\n            // default error msg will show modal\n            Modal.confirm({\n              content: msg,\n              showConfirm: false,\n              cancelText: 'close',\n            });\n            return Promise.reject(false);\n          }\n        }\n        // 401: Re-login required\n        if (status === 401) {\n          // clear userinfo\n          errorCodeStore.getState().reset();\n          loggedUserInfoStore.getState().clear();\n          floppyNavigation.navigateToLogin();\n          return Promise.reject(false);\n        }\n\n        if (status === 403) {\n          // Permission interception\n          if (data?.type === 'url_expired') {\n            // url expired\n            floppyNavigation.navigate(RouteAlias.activationFailed, {\n              handler: 'replace',\n            });\n            return Promise.reject(false);\n          }\n          if (data?.type === 'inactive') {\n            // inactivated\n            floppyNavigation.navigate(RouteAlias.inactive);\n            return Promise.reject(false);\n          }\n\n          if (data?.type === 'suspended') {\n            loggedUserInfoStore.getState().clear();\n            floppyNavigation.navigate(RouteAlias.suspended, {\n              handler: 'replace',\n            });\n            return Promise.reject(false);\n          }\n\n          if (isIgnoredPath(IGNORE_PATH_LIST)) {\n            return Promise.reject(false);\n          }\n          if (error.config?.url.includes('/admin/api')) {\n            errorCodeStore.getState().update('403');\n            return Promise.reject(false);\n          }\n\n          if (msg) {\n            toastStore.getState().show({\n              msg,\n              variant: 'danger',\n            });\n          }\n          return Promise.reject(false);\n        }\n\n        if (status === 404 && error.config?.allow404) {\n          if (isIgnoredPath(IGNORE_PATH_LIST)) {\n            return Promise.reject(false);\n          }\n          errorCodeStore.getState().update('404');\n          return Promise.reject(false);\n        }\n\n        if (status >= 500) {\n          if (isIgnoredPath(IGNORE_PATH_LIST)) {\n            return Promise.reject(false);\n          }\n\n          if (error.config?.ignoreError !== '50X') {\n            errorCodeStore.getState().update('50X');\n          }\n\n          console.error(\n            `Request failed with status code ${status}, ${msg || ''}`,\n          );\n        }\n        return Promise.reject(errorObject);\n      },\n    );\n  }\n\n  public request(config: AxiosRequestConfig): Promise<AxiosResponse> {\n    return this.instance.request(config);\n  }\n\n  public get<T = any>(url: string, config?: ApiConfig): Promise<T> {\n    return this.instance.get(url, config);\n  }\n\n  public post<T = any>(\n    url: string,\n    data?: any,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.instance.post(url, data, config);\n  }\n\n  public put<T = any>(\n    url: string,\n    data?: any,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.instance.put(url, data, config);\n  }\n\n  public delete<T = any>(\n    url: string,\n    data?: any,\n    config?: AxiosRequestConfig,\n  ): Promise<T> {\n    return this.instance.delete(url, {\n      data,\n      ...config,\n    });\n  }\n}\n\nexport default new Request(baseConfig);\n"
  },
  {
    "path": "ui/src/utils/requestAi.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { Modal } from '@/components';\nimport { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores';\nimport { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';\nimport { RouteAlias } from '@/router/alias';\nimport { getCurrentLang } from '@/utils/localize';\nimport Storage from '@/utils/storage';\nimport { floppyNavigation } from '@/utils/floppyNavigation';\nimport { isIgnoredPath, IGNORE_PATH_LIST } from '@/utils/guard';\n\ninterface RequestAiOptions extends RequestInit {\n  onMessage?: (text: any) => void;\n  onError?: (error: Error) => void;\n  onComplete?: () => void;\n  signal?: AbortSignal;\n  // 添加项目配置选项\n  allow404?: boolean;\n  ignoreError?: '403' | '50X';\n  passingError?: boolean;\n}\n\n// create a object to track the current request state\nconst requestState = {\n  currentReader: null as ReadableStreamDefaultReader<Uint8Array> | null,\n  abortController: null as AbortController | null,\n  isProcessing: false,\n};\n\n// HTTP error handling function (based on request.ts logic)\nconst handleHttpError = async (\n  response: Response,\n  options: RequestAiOptions,\n): Promise<void> => {\n  const { status } = response;\n  let errBody: any = {};\n\n  try {\n    const text = await response.text();\n    errBody = text ? JSON.parse(text) : {};\n  } catch {\n    errBody = { msg: response.statusText };\n  }\n\n  const { data = {}, msg = '', config } = errBody || {};\n\n  const errorObject = {\n    code: status,\n    msg,\n    data,\n  };\n\n  if (status === 400) {\n    if (data?.err_type && options?.passingError) {\n      return Promise.reject(errorObject);\n    }\n\n    if (data?.err_type) {\n      if (data.err_type === 'toast') {\n        toastStore.getState().show({\n          msg,\n          variant: 'danger',\n        });\n      }\n\n      if (data.err_type === 'alert') {\n        return Promise.reject({ msg, ...data });\n      }\n\n      if (data.err_type === 'modal') {\n        Modal.confirm({\n          content: msg,\n        });\n      }\n      return Promise.reject(false);\n    }\n\n    if (Array.isArray(data) && data.length > 0) {\n      return Promise.reject({\n        ...errorObject,\n        isError: true,\n        list: data,\n      });\n    }\n\n    if (!data || Object.keys(data).length <= 0) {\n      Modal.confirm({\n        content: msg,\n        showConfirm: false,\n        cancelText: 'close',\n      });\n      return Promise.reject(false);\n    }\n  }\n\n  // 401: 重新登录\n  if (status === 401) {\n    errorCodeStore.getState().reset();\n    loggedUserInfoStore.getState().clear();\n    floppyNavigation.navigateToLogin();\n    return Promise.reject(false);\n  }\n\n  if (status === 403) {\n    // Permission interception\n    if (data?.type === 'url_expired') {\n      // url expired\n      floppyNavigation.navigate(RouteAlias.activationFailed, {\n        handler: 'replace',\n      });\n      return Promise.reject(false);\n    }\n    if (data?.type === 'inactive') {\n      // inactivated\n      floppyNavigation.navigate(RouteAlias.inactive);\n      return Promise.reject(false);\n    }\n\n    if (data?.type === 'suspended') {\n      loggedUserInfoStore.getState().clear();\n      floppyNavigation.navigate(RouteAlias.suspended, {\n        handler: 'replace',\n      });\n      return Promise.reject(false);\n    }\n\n    if (isIgnoredPath(IGNORE_PATH_LIST)) {\n      return Promise.reject(false);\n    }\n    if (config?.url.includes('/admin/api')) {\n      errorCodeStore.getState().update('403');\n      return Promise.reject(false);\n    }\n\n    if (msg) {\n      toastStore.getState().show({\n        msg,\n        variant: 'danger',\n      });\n    }\n    return Promise.reject(false);\n  }\n\n  if (status === 404 && config?.allow404) {\n    if (isIgnoredPath(IGNORE_PATH_LIST)) {\n      return Promise.reject(false);\n    }\n    errorCodeStore.getState().update('404');\n    return Promise.reject(false);\n  }\n\n  if (status >= 500) {\n    if (isIgnoredPath(IGNORE_PATH_LIST)) {\n      return Promise.reject(false);\n    }\n\n    if (config?.ignoreError !== '50X') {\n      errorCodeStore.getState().update('50X');\n    }\n\n    console.error(`Request failed with status code ${status}, ${msg || ''}`);\n  }\n  return Promise.reject(errorObject);\n};\nconst requestAi = async (url: string, options: RequestAiOptions) => {\n  try {\n    // if there is a previous request being processed, cancel it\n    if (requestState.isProcessing && requestState.abortController) {\n      requestState.abortController.abort();\n    }\n\n    // create a new AbortController\n    const abortController = new AbortController();\n    requestState.abortController = abortController;\n\n    // merge the incoming signal with the new created signal\n    const combinedSignal = options.signal || abortController.signal;\n\n    // mark as being processed\n    requestState.isProcessing = true;\n\n    // get the authentication information and language settings (consistent with request.ts)\n    const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || '';\n    console.log(token);\n    const lang = getCurrentLang();\n\n    const response = await fetch(url, {\n      ...options,\n      method: 'POST',\n      signal: combinedSignal,\n      headers: {\n        Authorization: token,\n        'Accept-Language': lang,\n        'Content-Type': 'application/json',\n        ...options.headers,\n      },\n    });\n\n    // unified error handling (based on request.ts logic)\n    if (!response.ok) {\n      await handleHttpError(response, options);\n      return;\n    }\n\n    const reader = response.body?.getReader();\n    if (!reader) {\n      throw new Error('ReadableStream not supported');\n    }\n\n    // store the current reader so it can be cancelled later\n    requestState.currentReader = reader;\n\n    const decoder = new TextDecoder();\n    let buffer = '';\n\n    const processStream = async (): Promise<void> => {\n      try {\n        const { value, done } = await reader.read();\n\n        if (done) {\n          options.onComplete?.();\n          requestState.isProcessing = false;\n          requestState.currentReader = null;\n          return;\n        }\n\n        const chunk = decoder.decode(value, { stream: true });\n        buffer += chunk;\n\n        const lines = buffer.split('\\n');\n        buffer = lines.pop() || '';\n\n        lines.forEach((line) => {\n          if (line.trim()) {\n            try {\n              // handle the special [DONE] signal\n              const cleanedLine = line.replace(/^data: /, '').trim();\n              if (cleanedLine === '[DONE]') {\n                return; // skip the [DONE] signal processing\n              }\n\n              if (cleanedLine) {\n                const parsedLine = JSON.parse(cleanedLine);\n                options.onMessage?.(parsedLine);\n              }\n            } catch (error) {\n              console.debug('Error parsing line:', line);\n            }\n          }\n        });\n\n        // check if it has been cancelled\n        if (combinedSignal.aborted) {\n          requestState.isProcessing = false;\n          requestState.currentReader = null;\n          throw new Error('Request was aborted');\n        }\n\n        await processStream();\n      } catch (error) {\n        if ((error as Error).message === 'Request was aborted') {\n          options.onComplete?.();\n        } else {\n          throw error; // rethrow other errors\n        }\n      }\n    };\n\n    await processStream();\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error ? error.message : 'Unknown error';\n\n    // if the error is caused by cancellation, do not treat it as an error\n    if (\n      errorMessage !== 'The user aborted a request' &&\n      errorMessage !== 'Request was aborted'\n    ) {\n      console.error('Request AI Error:', errorMessage);\n      options.onError?.(new Error(errorMessage));\n    } else {\n      console.log('Request was cancelled by user');\n      options.onComplete?.(); // cancellation is also considered complete\n    }\n  } finally {\n    requestState.isProcessing = false;\n    requestState.currentReader = null;\n  }\n};\n\n// add a function to cancel the current request\nconst cancelCurrentRequest = () => {\n  if (requestState.abortController) {\n    requestState.abortController.abort();\n    console.log('AI request cancelled by user');\n    return true;\n  }\n  return false;\n};\n\nexport { cancelCurrentRequest };\nexport default requestAi;\n"
  },
  {
    "path": "ui/src/utils/saveDraft.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport debounce from 'lodash/debounce';\n\nimport {\n  DRAFT_QUESTION_STORAGE_KEY,\n  DRAFT_ANSWER_STORAGE_KEY,\n} from '@/common/constants';\nimport { storageExpires as storage } from '@/utils';\n\nexport type QuestionDraft = {\n  params: {\n    title: string;\n    content: string;\n    tags: any[];\n    answer_content: string;\n  };\n  callback?: () => void;\n};\n\nexport type AnswerDraft = {\n  questionId: string;\n  content: string;\n  callback?: () => void;\n};\n\ntype DraftType = {\n  type: 'question' | 'answer';\n};\n\nexport type DraftParams = QuestionDraft | AnswerDraft;\n\nclass SaveDraft {\n  type: DraftType['type'];\n\n  status: 'save' | 'remove';\n\n  constructor({ type = 'question' }: DraftType) {\n    this.type = type;\n    this.status = 'save';\n  }\n\n  save = debounce((data: DraftParams) => {\n    // TODO\n    if (this.status === 'remove') {\n      return;\n    }\n    if (this.type === 'question') {\n      const { params, callback } = data as QuestionDraft;\n\n      this.storeDraft(params, callback);\n    }\n\n    if (this.type === 'answer') {\n      const { content, questionId, callback } = data as AnswerDraft;\n      if (!questionId || !content) {\n        return;\n      }\n\n      this.storeDraft({ content, questionId }, callback);\n    }\n  }, 3000);\n\n  remove() {\n    this.status = 'remove';\n    const that = this;\n    if (this.type === 'question') {\n      storage.remove(DRAFT_QUESTION_STORAGE_KEY, () => {\n        that.status = 'save';\n      });\n    }\n    if (this.type === 'answer') {\n      storage.remove(DRAFT_ANSWER_STORAGE_KEY, () => {\n        that.status = 'save';\n      });\n    }\n  }\n\n  private storeDraft = (params: any, callback) => {\n    const key =\n      this.type === 'question'\n        ? DRAFT_QUESTION_STORAGE_KEY\n        : DRAFT_ANSWER_STORAGE_KEY;\n    storage.set(key, params);\n    callback?.();\n  };\n}\n\nexport default SaveDraft;\n"
  },
  {
    "path": "ui/src/utils/storage.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nconst Storage = {\n  get: (key: string): any => {\n    const value = localStorage.getItem(key);\n    if (value) {\n      try {\n        return JSON.parse(value);\n      } catch {\n        return value;\n      }\n    }\n    return undefined;\n  },\n  set: (key: string, value: any): void => {\n    if (typeof value === 'string') {\n      localStorage.setItem(key, value);\n      return;\n    }\n    localStorage.setItem(key, JSON.stringify(value));\n  },\n  remove: (key: string): void => {\n    localStorage.removeItem(key);\n  },\n  clear: (): void => {\n    localStorage.clear();\n  },\n};\n\nexport default Storage;\n"
  },
  {
    "path": "ui/src/utils/storageWithExpires.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { DRAFT_TIMESIGH_STORAGE_KEY as timeSign } from '@/common/constants';\n\nconst store = {\n  storage: localStorage || window.localStorage,\n  set(key: string, value, time?: number): void {\n    const t = time || Date.now() + 1000 * 60 * 60 * 24 * 7; // default 7 days\n    try {\n      this.storage.setItem(key, `${t}${timeSign}${JSON.stringify(value)}`);\n    } catch {\n      // ignore\n      console.error('set storage error: the key is', key);\n    }\n  },\n  get(key: string): any {\n    const timeSignLen = timeSign.length;\n    let index = 0;\n    let time = 0;\n    let res;\n    try {\n      res = this.storage.getItem(key);\n    } catch {\n      console.error('get storage error: the key is', key);\n    }\n    if (res) {\n      index = res.indexOf(timeSign);\n      time = +res.slice(0, index);\n      if (time > new Date().getTime()) {\n        res = res.slice(index + timeSignLen);\n        try {\n          res = JSON.parse(res);\n        } catch {\n          // ignore\n        }\n      } else {\n        // timeout remove storage\n        res = null;\n        this.storage.removeItem(key);\n      }\n      return res;\n    }\n\n    return res;\n  },\n  remove(key: string, callback?: () => void): void {\n    this.storage.removeItem(key);\n    callback?.();\n  },\n};\n\nexport default store;\n"
  },
  {
    "path": "ui/src/utils/userCenter.ts",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\nimport { RouteAlias } from '@/router/alias';\nimport { userCenterStore } from '@/stores';\nimport { getUcAgent, UcAgent } from '@/services/user-center';\n\nexport const pullUcAgent = async () => {\n  const uca = await getUcAgent();\n  userCenterStore.getState().update(uca);\n};\n\nexport const getLoginUrl = (uca?: UcAgent) => {\n  let ret = RouteAlias.login;\n  uca ||= userCenterStore.getState().agent;\n  if (\n    uca?.enabled &&\n    !uca.agent_info?.enabled_original_user_system &&\n    uca.agent_info?.login_redirect_url\n  ) {\n    ret = uca.agent_info.login_redirect_url;\n  }\n  return ret;\n};\n\nexport const getSignUpUrl = (uca?: UcAgent) => {\n  let ret = RouteAlias.signUp;\n  uca ||= userCenterStore.getState().agent;\n  if (uca?.enabled && uca?.agent_info?.sign_up_redirect_url) {\n    ret = uca.agent_info.sign_up_redirect_url;\n  }\n  return ret;\n};\n"
  },
  {
    "path": "ui/static.go",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements.  See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership.  The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License.  You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations\n * under the License.\n */\n\npackage ui\n\nimport (\n\t\"embed\"\n)\n\n//go:embed build\nvar Build embed.FS\n\n//go:embed template\nvar Template embed.FS\n"
  },
  {
    "path": "ui/template/404.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n            <div class=\"d-flex flex-column justify-content-center align-items-center page-wrap container\">\n                <div class=\"mb-4 text-secondary\" style=\"font-size: 120px; line-height: 1.2;\">(=‘x‘=)</div>\n                <div class=\"text-center mb-4\">Unfortunately, this page doesn't exist.</div>\n                <div class=\"text-center\"><a role=\"button\" tabindex=\"0\" href=\"{{$.baseURL}}/\" class=\"btn btn-link\">Back to\n                        homepage</a></div>\n            </div>\n\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/template/comment.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"comment\"}}\n{{range .comments}}\n<div class=\"border-bottom py-2 comment-item border-top\">\n  <div class=\"d-block\">\n    <div class=\"fmt small\">\n      {{formatLinkNofollow .ParsedText}}\n    </div>\n  </div>\n  <div class=\"d-flex justify-content-between small\">\n    <div class=\"d-flex align-items-center link-secondary\">\n      <a href=\"{{$.baseURL}}/users/{{.Username}}\">{{.UserDisplayName}}</a><span\n      class=\"mx-1\">•</span>\n      <time\n        class=\"me-3\"\n        datetime=\"{{timeFormatISO $.timezone .CreatedAt}}\"\n        title=\"{{translatorTimeFormatLongDate $.language $.timezone .CreatedAt}}\">{{translatorTimeFormat $.language $.timezone .CreatedAt}}\n      </time>\n      <button type=\"button\"\n              class=\"me-3 btn-no-border p-0 link-secondary btn btn-link btn-sm\">\n        <i class=\"br bi-hand-thumbs-up-fill\"></i>\n        {{if ne 0 .VoteCount}}\n        <span class=\"ms-2\">{{.VoteCount}}</span>\n        {{end}}\n      </button>\n      <button type=\"button\"\n              class=\"link-secondary m-0 p-0 btn-no-border btn btn-link btn-sm\">\n        {{translator $.language \"ui.comment.btn_reply\"}}\n      </button>\n    </div>\n\n    <div class=\"d-block d-md-none dropdown\">\n      <div class=\"no-toggle dropdown-toggle\" id=\"dropdown-comment\"\n           aria-expanded=\"false\" variant=\"success\">\n        <i class=\"br bi-three-dots text-secondary\"></i>\n      </div>\n    </div>\n  </div>\n</div>\n{{end}}\n{{end}}\n"
  },
  {
    "path": "ui/template/footer.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"footer\"}}\n        <div class=\"main-mx-with\">\n          <div class=\"row\">\n            <footer class=\"py-3 d-flex flex-wrap align-items-center justify-content-between text-secondary small\">\n              <div class=\"d-flex align-items-center\">\n                {{$cc := (join \" \" .siteinfo.General.Name .siteinfo.Year)}}\n                <div className=\"me-3\">{{- $ft := translator $.language \"ui.footer.build_on\" \"cc\" $cc -}}</div>\n                <a class=\"me-3\" href=\"{{$.baseURL}}/tos\" data-discover=\"true\" class=\"link-secondary\">\n                  {{translator $.language \"ui.admin.legal.terms_of_service.label\"}}\n                </a>\n                <a href=\"{{$.baseURL}}/privacy\" data-discover=\"true\" class=\"link-secondary\">\n                  {{translator $.language \"ui.admin.legal.privacy_policy.label\"}}\n                </a>\n              </div>\n              <div class=\"text-center mb-0 small text-secondary link-secondary\">\n                {{templateHTML (replaceHTMLTag $ft \"<a href=\\\"https://answer.apache.org/\\\" target=\\\"_blank\\\"> Answer </a>\")}}\n              </div>\n            </footer>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n<!--customize_footer-->\n{{if .FooterCode }}{{.FooterCode | templateHTML}}{{end}}\n<!--customize_footer-->\n</body>\n</html>\n{{end}}\n"
  },
  {
    "path": "ui/template/header.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"header\"}}\n<!DOCTYPE html>\n<html lang=\"{{.lang}}\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>{{.title}}</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n    <meta name=\"theme-color\" />\n    <meta name=\"description\" content=\"{{.description}}\" data-rh=\"true\" />\n    <meta name=\"generator\" content=\"Answer {{.Version}} - https://github.com/apache/answer version {{.Revision}}\">\n    {{if .keywords }}<meta name=\"keywords\" content=\"{{.keywords}}\" data-rh=\"true\" />{{end}}\n    {{if .noindex }}<meta name=\"robots\" content=\"noindex\">{{end}}\n\n    <link rel=\"canonical\" href=\"{{.siteinfo.Canonical}}\" />\n    <link rel=\"manifest\" href=\"{{$.baseURL}}/manifest.json\" />\n    <link rel=\"search\" type=\"application/opensearchdescription+xml\" href=\"{{$.baseURL}}/opensearch.xml\" title=\"{{.siteinfo.General.Name}}\" />\n    <link href=\"{{.cssPath}}\" rel=\"stylesheet\" />\n    <link href=\"{{$.baseURL}}/custom.css\" rel=\"stylesheet\" />\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      href=\"{{if $.siteinfo.Branding.Favicon }}{{$.siteinfo.Branding.Favicon}}{{else}}{{$.baseURL}}/favicon.ico{{end}}\"\n      data-rh=\"true\"\n    />\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      sizes=\"192x192\"\n      href=\"{{.siteinfo.Branding.SquareIcon}}\"\n      data-rh=\"true\"\n    />\n    <link\n      rel=\"apple-touch-icon\"\n      type=\"image/png\"\n      href=\"{{.siteinfo.Branding.SquareIcon}}\"\n      data-rh=\"true\"\n    />\n    {{range $path := .scriptPath}}\n    <script defer=\"defer\" src=\"{{$path}}\"></script>\n    {{end}}\n    {{if $.siteinfo.JsonLD }}{{ .siteinfo.JsonLD | templateHTML}}{{end}}\n\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:title\" name=\"twitter:title\" content=\"{{.title}}\" />\n    <meta property=\"og:site_name\" content=\"{{.siteinfo.General.Name}}\" />\n    <meta property=\"og:url\" content=\"{{.siteinfo.Canonical}}\" />\n    <meta property=\"og:description\" content=\"{{.description}}\" />\n    <meta\n            property=\"og:image\"\n            itemProp=\"image primaryImageOfPage\"\n            content=\"{{if $.siteinfo.Branding.Favicon }}{{$.siteinfo.Branding.Favicon}}{{else}}{{$.baseURL}}/favicon.ico{{end}}\"\n    />\n    <meta name=\"twitter:card\" content=\"summary\" />\n    <meta name=\"twitter:domain\" content=\"{{.siteinfo.General.SiteUrl}}\" />\n    <meta name=\"twitter:description\" content=\"{{.description}}\" />\n    <meta\n            name=\"twitter:image\"\n            content=\"{{if $.siteinfo.Branding.Favicon }}{{$.siteinfo.Branding.Favicon}}{{else}}{{$.baseURL}}/favicon.ico{{end}}\"\n    />\n    <meta name=\"go-template\">\n    <!--customize_head-->\n    {{if .HeadCode }} {{.HeadCode | templateHTML}} {{end}}\n    <!--customize_head-->\n  </head>\n\n  <body>\n    <!--customize_header-->\n    {{if .HeaderCode }} {{.HeaderCode | templateHTML}} {{end}}\n    <!--customize_header-->\n    <div id=\"root\">\n\n      <div id=\"spin-mask\">\n        <noscript>\n          <style>\n            #spin-mask {\n              display: none !important;\n            }\n\n            #protect-browser {\n              display: none;\n            }\n          </style>\n        </noscript>\n        <style>\n          @keyframes _doc-spin {\n            to { transform: rotate(360deg) }\n          }\n          #spin-mask {\n            position: fixed;\n            top: 0;\n            right: 0;\n            bottom: 0;\n            left: 0;\n            background-color: white;\n            z-index: 9999;\n          }\n          #spin-container {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n          }\n          #spin-container .spinner {\n            box-sizing: border-box;\n            display: inline-block;\n            width: 2rem;\n            height: 2rem;\n            vertical-align: -.125em;\n            border: .25rem solid currentColor;\n            border-right-color: transparent;\n            color: rgba(108, 117, 125, .75);\n            border-radius: 50%;\n            animation: 0.75s linear infinite _doc-spin;\n          }\n\n          #protect-browser {\n            padding: 20px;\n            text-align: center;\n          }\n        </style>\n        <div id=\"spin-container\">\n          <div class=\"spinner\"></div>\n        </div>\n        <div id=\"protect-browser\"></div>\n      </div>\n\n      <nav id=\"header\" class=\"sticky-top theme-dark navbar navbar-expand-xl navbar-light\">\n        <div class=\"w-100 d-flex align-items-center px-3\">\n          <button\n            aria-controls=\"navBarContent\"\n            type=\"button\"\n            aria-label=\"Toggle navigation\"\n            class=\"answer-navBar me-2 navbar-toggler collapsed\"\n          >\n            <span class=\"navbar-toggler-icon\"></span>\n          </button>\n          <div\n            class=\"d-flex justify-content-between align-items-center nav-grow flex-nowrap\"\n          >\n            {{if .siteinfo.Branding.Logo}}\n              <a class=\"lh-1 me-0 me-sm-5 p-0 navbar-brand\" href=\"{{$.baseURL}}/\">\n                <img class=\"logo me-0\" src=\"{{.siteinfo.Branding.Logo}}\" alt=\"{{.siteinfo.General.Name}}\">\n              </a>\n            {{else}}\n              <a class=\"lh-1 me-0 me-sm-3 navbar-brand\" href=\"{{$.baseURL}}/\">\n                {{.siteinfo.General.Name}}\n              </a>\n            {{end}}\n          </div>\n          <div class=\"d-none d-xl-block flex-grow-1 me-auto\">\n            <div class=\"d-none d-xl-block ps-0 col-lg-8\">\n              <form action=\"/search\" class=\"w-100 maxw-400\">\n                <input placeholder=\"Search\" name=\"q\" type=\"search\" class=\"placeholder-search form-control\" value=\"\">\n              </form>\n            </div>\n            <!-- <div class=\"xl-none mt-3 pb-1 nav-item\">\n              <a class=\"text-capitalize text-nowrap btn btn-light\" href=\"/questions/add\">{{translator $.language \"ui.btns.add_question\"}}</a>\n            </div> -->\n            <div class=\"d-none d-xl-flex justify-content-start justify-content-sm-end col-lg-4\">\n              <!-- <a role=\"button\" tabindex=\"0\" href=\"/users/login\" class=\"me-2 link-light btn btn-link\">\n                {{translator $.language \"ui.btns.login\"}}\n              </a>\n              <a role=\"button\" tabindex=\"0\" href=\"/users/register\" class=\"btn btn-light\">\n                {{translator $.language \"ui.btns.signup\"}}\n              </a> -->\n            </div>\n          </div>\n        </div>\n      </nav>\n      <div class=\"position-relative page-wrap d-flex flex-column flex-fill\">\n        <div class=\"d-flex\">\n          <div class=\"position-sticky px-3 border-end pt-4 d-none d-xl-block\" id=\"pcSideNav\">\n            {{template \"sidenav\" . }}\n          </div>\n          <div class=\"flex-fill w-100 overflow-x-hidden\">\n            {{end}}\n          </div>\n        </div>\n      </div>\n\n    </div>\n  </body>\n  <script>\n    /**\n     * @description: Prompt that the browser version is too low\n     */\n    const defaultList = [\n      {\n        name: 'Edge',\n        version: '100'\n      },\n      {\n        name: 'Firefox',\n        version: '100'\n      },\n      {\n        name: 'Chrome',\n        version: '90'\n      },\n      {\n        name: 'Safari',\n        version: '15'\n      },\n      {\n        name: 'IE',\n      }\n    ];\n    function getBrowserTypeAndVersion(){\n      var browser = {\n        name: '',\n        version: ''\n      };\n      var ua = navigator.userAgent.toLowerCase();\n      var s;\n      ((ua.indexOf(\"compatible\") > -1 && ua.indexOf(\"MSIE\") > -1) || (ua.indexOf('Trident') > -1 && ua.indexOf(\"rv:11.0\") > -1)) ? browser = { name: 'IE', version: '' } :\n        (s = ua.match(/edge\\/([\\d\\.]+)/)) ? browser = { name: 'Edge', version: s[1] } :\n          (s = ua.match(/firefox\\/([\\d\\.]+)/)) ? browser = { name: 'Firefox', version: s[1] } :\n            (s = ua.match(/chrome\\/([\\d\\.]+)/)) ?  browser = { name: 'Chrome', version: s[1] } :\n              (s = ua.match(/version\\/([\\d\\.]+).*safari/)) ?  browser = { name: 'Safari', version: s[1] } : browser = { name: 'unknown', version: '' };\n      // 根据关系进行判断\n      return browser;\n    }\n\n    function compareVersion(version1, version2) {\n      var v1 = version1.split('.');\n      var v2 = version2.split('.');\n      var len = Math.max(v1.length, v2.length);\n      while (v1.length < len) {\n        v1.push('0');\n      }\n      while (v2.length < len) {\n        v2.push('0');\n      }\n      for (var i = 0; i < len; i++) {\n        var num1 = parseInt(v1[i]);\n        var num2 = parseInt(v2[i]);\n        if (num1 >= num2) {\n          return 1;\n        } else if (num1 < num2) {\n          return -1;\n        }\n      }\n      return 0;\n    }\n\n    const browserInfo = getBrowserTypeAndVersion();\n\n    if (browserInfo.name === 'IE') {\n      const div = document.getElementById('protect-browser');\n      div.innerText = 'Sorry, this site does not support Internet Explorer. In order to avoid affecting the normal use of our features, please use a more modern browser such as Edge, Firefox, Chrome, or Safari.'\n    } else {\n      const notSupport = defaultList.some(item => {\n        if (item.name === browserInfo.name) {\n          return compareVersion(browserInfo.version, item.version) === -1;\n        }\n        return false;\n      });\n      if (notSupport) {\n        const div = document.getElementById('protect-browser');\n        div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'\n      }\n    }\n\n  </script>\n\n</html>\n"
  },
  {
    "path": "ui/template/homepage.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n<div class=\"d-flex justify-content-center px-0 px-md-4\">\n  <div class=\"answer-container\">\n    <div class=\"pt-4 mb-5\">\n      <div class=\"row\">\n        <div class=\"col\">\n          <div class=\"d-md-flex d-block flex-wrap justify-content-between\">\n            <div class=\"d-flex flex-column flex-md-row mb-4\">\n              <a href=\"{{$.baseURL}}/users/{{.userinfo.Username}}\">\n                <img\n                    src=\"{{.userinfo.Avatar}}\"\n                    width=\"160px\" height=\"160px\" class=\"rounded\" alt=\"{{.userinfo.Username}}\" />\n                </a>\n              <div class=\"ms-0 ms-md-4 mt-4 mt-md-0\">\n                <div class=\"d-flex align-items-center mb-2\">\n                    <a class=\"link-dark h3 mb-0\" href=\"{{$.baseURL}}/users/{{.userinfo.Username}}\">{{.userinfo.DisplayName}}</a>\n\n                </div>\n                <div class=\"text-secondary mb-4\">@{{.userinfo.Username}}</div>\n                <div class=\"d-flex flex-wrap mb-3\">\n                    <div class=\"me-3\">\n                        <strong class=\"fs-5\">{{.userinfo.Rank}}</strong><span class=\"text-secondary\"> {{translator $.language \"ui.personal.x_reputation\"}}</span>\n                    </div>\n                    <div class=\"me-3\">\n                        <strong class=\"fs-5\">{{.userinfo.AnswerCount}}</strong><span class=\"text-secondary\"> {{translator $.language \"ui.personal.x_answers\"}}</span>\n                    </div>\n                    <div>\n                        <strong class=\"fs-5\">{{.userinfo.QuestionCount}}</strong><span class=\"text-secondary\"> {{translator $.language \"ui.personal.x_questions\"}}</span>\n                    </div>\n                </div>\n                {{if .userinfo.Website }}\n                <div class=\"d-flex align-items-center\"><i class=\"br bi-house-door-fill me-2\"></i><a class=\"link-secondary\" href=\"{{.userinfo.Website}}\">{{.userinfo.Website}}</a></div>\n                {{else}}\n                {{end}}\n                <div class=\"d-flex text-secondary\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div class=\"justify-content-center row\">\n\n          <div class=\"col-xxl-7 col-lg-8 col-sm-12\">\n              <div>\n                  <h5 class=\"mb-3\">{{translator $.language \"ui.personal.about_me\"}}</h5>\n                  {{if .bio }}\n                  <div class=\"mb-5 text-break fmt\">{{.bio}}</div>\n                  {{else}}\n                  <div class=\"text-center py-5 mb-4\">{{translator $.language \"ui.personal.about_me_empty\"}}</div>\n                  {{end}}\n                  <div class=\"mb-4 row\">\n                      <div class=\"mb-4 col-md-6 col-sm-12\">\n                          <h5 class=\"mb-3\">Top Answers</h5>\n                          <ol class=\"list-unstyled\">\n                              {{ range .topAnswers }}\n                              <li class=\"mb-2\">\n                                  <a class=\"text-truncate-1\" href=\"{{$.baseURL}}/questions/{{.QuestionID}}/{{.AnswerID}}\">{{.QuestionInfo.Title}}</a>\n                                  <div class=\"text-secondary small\">\n                                      <i class=\"br bi-hand-thumbs-up-fill me-1\"></i><span>{{.VoteCount}} votes</span>\n                                  </div>\n                              </li>\n                              {{ end }}\n                          </ol>\n                      </div>\n                      <div class=\"col-md-6 col-sm-12\">\n                          <h5 class=\"mb-3\">Top Questions</h5>\n                          <ol class=\"list-unstyled\">\n                              {{ range .topQuestions }}\n                              <li class=\"mb-2\">\n                                  <a class=\"text-truncate-1\" href=\"{{$.baseURL}}/questions/{{.ID}}\">{{.Title}}</a>\n                                  <div class=\"text-secondary small\">\n                                      <i class=\"br bi-hand-thumbs-up-fill me-1\"></i><span> {{.VoteCount}} votes</span>\n                                      <div class=\"d-inline-block text-secondary ms-3 small text-success\">\n                                          <i class=\"br bi-check-circle-fill\"></i><span> {{.AnswerCount}} answers</span>\n                                      </div>\n                                  </div>\n                              </li>\n                              {{ end }}\n                          </ol>\n                      </div>\n                  </div>\n              </div>\n          </div>\n          <div class=\"mt-5 mt-lg-0 col-xxl-3 col-lg-4 col-sm-12\">\n\n          </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/template/hot-question.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"hot-question\"}}\n<div class=\"card\">\n  <div class=\"text-nowrap text-capitalize card-header\">{{translator $.language \"ui.question.hot_questions\"}}</div>\n  <div class=\"list-group list-group-flush\">\n  {{ range .hotQuestion }}\n    {{if $.useTitle }}\n    <a class=\"list-group-item list-group-item-action\" href=\"{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}\">\n    {{else}}\n    <a class=\"list-group-item list-group-item-action\" href=\"{{$.baseURL}}/questions/{{.ID}}\">\n    {{end}}\n      <div class=\"link-dark\">{{ .Title }}</div>\n      {{if ne 0 .AnswerCount}}\n      <div class=\"d-flex align-items-center small mt-1 link-secondary\">\n        <i class=\"br bi-chat-square-text-fill\"></i>\n        <span class=\"ms-1\">{{translator $.language \"ui.question.x_answers\" \"count\" .AnswerCount}}</span>\n      </div>\n      {{end}}\n    </a>\n  {{ end }}\n  </div>\n</div>\n{{end}}\n"
  },
  {
    "path": "ui/template/opensearch.xml",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n  <ShortName>{{$.general.Name}}</ShortName>\n  {{if $.general.Description}}\n  <Description>{{$.general.Description}}</Description>\n  {{end}}\n  <InputEncoding>UTF-8</InputEncoding>\n  <Image width=\"16\" height=\"16\" type=\"image/x-icon\">{{$.favicon}}</Image>\n  <Url type=\"text/html\" method=\"get\" template=\"{{$.general.SiteUrl}}/search?q={searchTerms}\"/>\n</OpenSearchDescription>\n"
  },
  {
    "path": "ui/template/page.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"page\"}}\n<ul class=\"d-inline-flex mb-0 pagination pagination-sm\">\n  {{ if ne .page.Currpage 1 }}\n  <li class=\"page-item\">\n    <a class=\"page-link\" href=\"{{$.path}}?page={{$.page.Prevpage}}\"><span aria-hidden=\"true\">{{translator $.language \"ui.pagination.prev\"}}</span><span\n        class=\"visually-hidden\">{{translator $.language \"ui.pagination.prev\"}}</span></a>\n  </li>\n  {{ end }}\n  {{ range $value := .page.Pages }}\n  {{ if eq $.page.Currpage $value }}\n  <li class=\"page-item active\">\n    <span class=\"page-link\" href=\"{{$.path}}?page={{$value}}\">{{$value}}\n      <span class=\"visually-hidden\">(current)</span>\n    </span>\n  </li>\n  {{ else }}\n  <li class=\"page-item\">\n    <a class=\"page-link\" href=\"{{$.path}}?page={{$value}}\">{{$value}}</a>\n  </li>\n  {{ end }}\n  {{ end }}\n  {{ if lt $.page.Currpage $.page.Totalpages }}\n  <li class=\"page-item\">\n    <a class=\"page-link\" href=\"{{$.path}}?page={{$.page.Nextpage}}\"><span aria-hidden=\"true\">{{translator $.language \"ui.pagination.next\"}}</span><span\n        class=\"visually-hidden\">{{translator $.language \"ui.pagination.next\"}}</span></a>\n  </li>\n  {{ end }}\n</ul>\n{{end}}\n"
  },
  {
    "path": "ui/template/question-detail.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n<div class=\"d-flex justify-content-center px-0 px-md-4\">\n  <div class=\"answer-container\">\n    <div class=\"questionDetailPage pt-4 mb-5 row\">\n      <div class=\"page-main flex-auto col\">\n        <div>\n          <h1 class=\"h3 mb-3 text-wrap text-break\">\n            {{if $.useTitle }}\n            <a class=\"link-dark\" href=\"{{$.baseURL}}/questions/{{.detail.ID}}/{{urlTitle .detail.Title}}\">{{.detail.Title}}</a>\n            {{else}}\n            <a class=\"link-dark\" href=\"{{$.baseURL}}/questions/{{.detail.ID}}\">{{.detail.Title}}</a>\n            {{end}}\n          </h1>\n          <div\n            class=\"d-flex flex-wrap align-items-center small mb-4 text-secondary border-bottom pb-3\">\n            <div class=\"d-flex align-items-center  text-secondary me-3\">\n              <a class=\"d-flex align-items-center\" href=\"{{$.baseURL}}/users/{{.detail.UserInfo.Username}}\">\n                <img\n                src=\"{{.detail.UserInfo.Avatar}}\"\n                width=\"24px\" height=\"24px\"\n                class=\"rounded me-1\"\n                alt=\"\"/>\n                <span class=\"me-1 name-ellipsis\" style=\"max-width: 300px;\">{{.detail.UserInfo.DisplayName}}</span>\n              </a>\n              <span class=\"fw-bold\" title=\"Reputation\">{{.detail.UserInfo.Rank}}</span>\n            </div>\n            <time class=\"me-3 link-secondary\"\n                  datetime=\"{{timeFormatISO $.timezone .detail.CreateTime}}\"\n                  title=\"{{translatorTimeFormatLongDate $.language $.timezone .detail.CreateTime}}\">{{translator $.language \"ui.question_detail.created\"}} {{translatorTimeFormat $.language $.timezone .detail.CreateTime}}\n            </time>\n            {{if gt .detail.QuestionUpdateTime 0}}\n              <time class=\"me-3 link-secondary\"\n                    datetime=\"{{timeFormatISO $.timezone .detail.QuestionUpdateTime}}\"\n                    title=\"{{translatorTimeFormatLongDate $.language $.timezone .detail.QuestionUpdateTime}}\">{{translator $.language \"ui.question_detail.Edited\"}} {{translatorTimeFormat $.language $.timezone .detail.QuestionUpdateTime}}\n              </time>\n            {{end}}\n            <div class=\"me-3\">{{translator $.language \"ui.question_detail.Views\"}} {{.detail.ViewCount}}</div>\n            <button type=\"button\" class=\"p-0 btn-no-border btn btn-link btn-sm\">{{translator $.language \"ui.question_detail.Follow\"}}</button>\n          </div>\n\n          <div class=\"img-viewer\">\n            <article class=\"fmt text-break text-wrap last-p mt-4\">\n              {{formatLinkNofollow .detail.HTML}}\n            </article>\n          </div>\n\n          <div class=\"m-n1\">\n            {{range .detail.Tags}}\n            <a href=\"{{$.baseURL}}/tags/{{.SlugName}}\"\n                class=\"badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1\">\n              <span class=\"\">{{.SlugName}}</span>\n            </a>\n            {{end}}\n          </div>\n\n          <div class=\"mt-4\">\n            <div role=\"group\" class=\"btn-group\">\n              <button type=\"button\" class=\"btn btn-outline-secondary\">\n                <i class=\"br bi-hand-thumbs-up-fill\"></i></button>\n              <button type=\"button\"\n                      disabled=\"\" class=\"btn btn-outline-dark text-body\">\n                {{.detail.VoteCount}}\n              </button>\n              <button type=\"button\" class=\"btn btn-outline-secondary\">\n                <i class=\"br bi-hand-thumbs-down-fill\"></i>\n              </button>\n            </div>\n            <button type=\"button\" class=\"btn btn-outline-secondary ms-3\">\n              <i class=\"br bi-bookmark-fill\"></i><span style=\"padding-left: 10px\">{{.detail.CollectionCount}}</span>\n            </button>\n          </div>\n\n          <div class=\"mt-4\">\n            <div class=\"d-flex flex-wrap justify-content-between align-items-center\"><div class=\"d-flex flex-wrap\"><button type=\"button\" class=\"rounded-pill me-2 link-secondary btn-reaction btn btn-light btn-sm\"><i class=\"br bi-chat-text-fill\"></i><span class=\"ms-1\">Add comment</span></button><button type=\"button\" aria-label=\"add or remove reactions\" aria-haspopup=\"true\" class=\"smile-btn rounded-pill link-secondary btn-reaction btn btn-light btn-sm\"><i class=\"br bi-emoji-smile-fill\"></i><span class=\"ms-1\">+</span></button></div><div class=\"md-show align-items-center\"><div class=\"dropdown\"><a class=\"no-toggle pointer d-flex link-secondary small dropdown-toggle\" id=\"dropdown-share\" aria-expanded=\"false\" style=\"line-height: 23px;\">{{translator $.language \"ui.share.name\"}}</a></div></div><div class=\"md-hide\"></div></div>\n\n            <div class=\"comments-wrap\">\n              {{template \"comment\" (wrapComments (index $.comments $.detail.ID) $.language $.timezone)}}\n            </div>\n          </div>\n\n        </div>\n        <div class=\"d-flex align-items-center justify-content-between mt-5 mb-3\"\n              id=\"answerHeader\">\n          <h5 class=\"mb-0\">{{.detail.AnswerCount}} Answers</h5>\n        </div>\n        {{range .answers}}\n        <div class=\"answer-item py-4\">\n          <article class=\"fmt\">\n            {{formatLinkNofollow .HTML}}\n          </article>\n          <div class=\"d-flex align-items-center mt-4\">\n            <div class=\"\">\n              <div role=\"group\" class=\"btn-group\">\n                <button type=\"button\" class=\"btn btn-outline-secondary\">\n                  <i class=\"br bi-hand-thumbs-up-fill\"></i></button>\n                <button type=\"button\"\n                        disabled=\"\" class=\"btn btn-outline-dark text-body\">\n                  {{.VoteCount}}\n                </button>\n                <button type=\"button\" class=\"btn btn-outline-secondary\">\n                  <i class=\"br bi-hand-thumbs-down-fill\"></i>\n                </button>\n              </div>\n            </div>\n            {{if eq 2 .Accepted}}\n            <button type=\"button\" disabled=\"\"\n                    class=\"ms-3 active opacity-100 bg-success text-white btn btn-outline-success\">\n              <i class=\"br bi-check-circle-fill me-2\"></i><span>{{translator $.language \"ui.question_detail.answers.btn_accepted\"}}</span>\n            </button>\n            {{end}}\n          </div>\n          <div class=\"mt-4 mb-3 row\">\n            <div class=\"mb-3 mb-md-0 col\">\n              <div class=\"d-flex align-items-center\">\n\n              </div>\n            </div>\n            <div class=\"mb-3 mb-md-0 col-lg-3\">\n              <a href=\"{{$.baseURL}}/posts/{{$.detail.ID}}/{{.ID}}/timeline\">\n                <time\n                  class=\"link-secondary small\"\n                  datetime=\"{{timeFormatISO $.timezone .UpdateTime}}\"\n                  title=\"{{translatorTimeFormatLongDate $.language $.timezone .UpdateTime}}\">\n                  {{translator $.language \"ui.question_detail.edit\"}} {{translatorTimeFormat $.language $.timezone .UpdateTime}}\n                </time>\n              </a>\n            </div>\n            <div class=\"col-lg-4\">\n              <div class=\"d-flex\">\n                <a href=\"{{$.baseURL}}/users/{{.UserInfo.Username}}\"><img\n                  src=\"{{.UserInfo.Avatar}}\"\n                  width=\"40px\" height=\"40px\"\n                  class=\"rounded me-2 d-none d-md-block\"\n                  alt=\"\"/><img\n                  src=\"{{.UserInfo.Avatar}}\"\n                  width=\"24px\" height=\"24px\"\n                  class=\"rounded me-2 d-block d-md-none\"\n                  alt=\"\"/></a>\n                <div\n                  class=\"small text-secondary d-flex flex-row flex-md-column align-items-center align-items-md-start\">\n                  <div class=\"me-1 me-md-0\">\n                    <a class=\"me-1 text-break\"\n                        href=\"{{$.baseURL}}/users/{{.UserInfo.Username}}\">{{.UserInfo.DisplayName}}</a><span\n                    class=\"fw-bold\" title=\"Reputation\">{{.UserInfo.Rank}}</span>\n                  </div>\n                  <a href=\"{{$.baseURL}}/posts/{{$.detail.ID}}/{{.ID}}/timeline\">\n                    <time\n                      class=\"link-secondary\"\n                      datetime=\"{{timeFormatISO $.timezone .CreateTime}}\"\n                      title=\"{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}\">{{translator $.language \"ui.question_detail.answered\"}} {{translatorTimeFormat $.language $.timezone .CreateTime}}\n                    </time>\n                  </a>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div class=\"comments-wrap\">\n            {{template \"comment\" (wrapComments (index $.comments .ID) $.language $.timezone)}}\n          </div>\n        </div>\n        {{end}}\n      </div>\n      <div class=\"page-right-side mt-4 mt-xl-0 col\">\n        {{template \"related-question\" .}}\n      </div>\n    </div>\n  </div>\n</div>\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/template/question.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n<div class=\"d-flex justify-content-center px-0 px-md-4\">\n  <div class=\"answer-container\">\n    <div class=\"pt-4 mb-5 row\">\n      <div class=\"page-main flex-auto overflow-x-hidden col\">\n        <div>\n          <div class=\"mb-3 d-flex flex-wrap justify-content-between\">\n            <h5 class=\"fs-5 text-nowrap mb-3 mb-md-0\">\n              {{translator $.language \"ui.question.all_questions\"}}\n            </h5>\n            {{template \"sort-btns\" .}}\n          </div>\n          <div class=\"rounded-0 list-group\">\n            {{range .data}}\n            <li class=\"py-3 px-2 border-start-0 border-end-0 position-relative pointer list-group-item list-group-item-action\">\n              <div class=\"d-flex flex-wrap text-secondary small mb-12\">\n                <div class=\"d-flex align-items-center  text-secondary me-1\">\n                  <a href=\"{{$.baseURL}}/users/{{.Operator.Username}}\">\n                    <img src=\"{{$.baseURL}}/users/{{.Operator.Avatar}}\" width=\"24px\" height=\"24px\" class=\"rounded-circle me-1\" alt=\"shuai\" data-processed=\"true\">\n                    <span class=\"me-1 name-ellipsis\" style=\"max-width: 300px;\">{{.Operator.DisplayName}}</span>\n                  </a>\n                  <span class=\"fw-bold\" title=\"Reputation\">{{.Operator.Rank}}</span>\n                </div>\n                •\n                <time\n                  class=\"text-secondary ms-1\"\n                  datetime=\"{{timeFormatISO $.timezone .OperatedAt}}\"\n                  title=\"{{translatorTimeFormatLongDate $.language $.timezone .OperatedAt}}\">\n                  {{translator $.language \"ui.question.asked\"}}\n                  {{translatorTimeFormat $.language $.timezone .OperatedAt}}\n                </time>\n              </div>\n\n              <h5 class=\"text-wrap text-break\">\n                {{if $.useTitle }}\n                <a class=\"link-dark d-block\" href=\"{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}\">\n                  {{.Title}}\n                </a>\n                {{else}}\n                <a class=\"link-dark d-block\" href=\"{{$.baseURL}}/questions/{{.ID}}\">{{.Title}}</a>\n                {{end}}\n              </h5>\n\n              <div class=\"text-truncate-2 mb-2\">\n                {{if $.useTitle }}\n                <a class=\"d-block small text-body\" href=\"{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}\">{{.Description}}\n                </a>\n                {{else}}\n                <a class=\"d-block small text-body\" href=\"{{$.baseURL}}/questions/{{.ID}}\">{{.Description}}</a>\n                {{end}}\n              </div>\n\n              <div class=\"question-tags mb-12\">\n                {{range .Tags }}\n                <a\n                  href=\"{{$.baseURL}}/tags/{{.SlugName}}\"\n                  class=\"badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} me-1\">\n                  <span class=\"\">{{.SlugName}}</span>\n                </a>\n                {{end}}\n              </div>\n\n              <div class=\"small text-secondary\">\n                <div class=\"d-flex align-items-center mt-2 mt-md-0\">\n                  <div class=\"d-flex align-items-center flex-shrink-0\">\n                    <i class=\"br bi-hand-thumbs-up-fill me-1\"></i>\n                    <span class=\"fw-medium\">{{.VoteCount}}</span>\n                    <span class=\"ms-1\">{{translator $.language \"ui.counts.votes\"}}</span>\n                  </div>\n                  <div class=\"d-flex flex-shrink-0 align-items-center ms-3\">\n                    <i class=\"br bi-chat-square-text-fill me-1\"></i>\n                    <span class=\"fw-medium\">{{.AnswerCount}}</span>\n                    <span class=\"ms-1\">{{translator $.language \"ui.counts.answers\"}}</span>\n                  </div>\n                  <span class=\"summary-stat ms-3 flex-shrink-0\">\n                    <i class=\"br bi-bar-chart-fill\"></i>\n                    <span class=\"fw-medium ms-1\">{{.ViewCount}}</span>\n                    <span class=\"ms-1\">{{translator $.language \"ui.counts.views\"}}</span>\n                  </span>\n                </div>\n              </div>\n            </li>\n            {{end}}\n          </div>\n          <div class=\"mt-4 mb-2 d-flex justify-content-center\">\n            {{template \"page\" .}}\n          </div>\n        </div>\n      </div>\n      <div class=\"page-right-side mt-4 mt-xl-0 col\">\n        {{template \"hot-question\" .}}\n      </div>\n    </div>\n  </div>\n</div>\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/template/related-question.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"related-question\"}}\n<div class=\"card\">\n  <div class=\"card-header\">{{translator $.language \"ui.related_question.title\"}}</div>\n  <div class=\"list-group list-group-flush\">\n    {{ range .relatedQuestion }}\n    {{if $.useTitle }}\n    <a class=\"list-group-item list-group-item-action\" href=\"{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}\">\n    {{else}}\n    <a class=\"list-group-item list-group-item-action\" href=\"{{$.baseURL}}/questions/{{.ID}}\">\n      {{end}}\n      <div class=\"link-dark\">{{ .Title }}</div>\n      {{if ne 0 .AnswerCount}}\n      <div class=\"mt-1 small me-2 link-secondary\">\n        <i class=\"br bi-chat-square-text-fill me-1\"></i>\n        <span>{{ .AnswerCount }} {{translator $.language \"ui.related_question.answers\"}}</span>\n      </div>\n      {{end}}\n    </a>\n    {{ end }}\n  </div>\n</div>\n{{end}}\n"
  },
  {
    "path": "ui/template/sidenav.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"sidenav\"}}\n<div class=\"flex-column nav nav-pills\">\n  <a class=\"nav-link active\" href=\"{{$.baseURL}}/questions\">\n    <i class=\"br bi-question-circle-fill me-2\"></i>\n    <span>{{translator $.language \"ui.header.nav.question\"}}</span>\n  </a>\n  <a href=\"{{$.baseURL}}/tags\" data-rr-ui-event-key=\"/tags\" class=\"nav-link\">\n    <i class=\"br bi-tags-fill me-2\"></i>\n    <span>{{translator $.language \"ui.header.nav.tag\"}}</span>\n  </a>\n  <a class=\"nav-link\" href=\"{{$.baseURL}}/users\">\n    <i class=\"br bi-people-fill me-2\"></i>\n    <span>{{translator $.language \"ui.header.nav.user\"}}</span>\n  </a>\n  <a class=\"nav-link\" href=\"{{$.baseURL}}/badges\">\n    <i class=\"br bi-people-fill me-2\"></i>\n    <span>{{translator $.language \"ui.header.nav.badges\"}}</span>\n  </a>\n</div>\n{{end}}\n"
  },
  {
    "path": "ui/template/sitemap-list.xml",
    "content": "{{ .xmlHeader }}\n<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  {{ range .page }}\n  <sitemap>\n    <loc>{{$.general.SiteUrl}}/sitemap/question-{{.}}.xml</loc>\n  </sitemap>\n  {{ end }}\n</sitemapindex>\n"
  },
  {
    "path": "ui/template/sitemap.xml",
    "content": "{{ .xmlHeader }}\n<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  {{ range .list }}\n  <url>\n  {{if $.hastitle}}\n    <loc>{{$.general.SiteUrl}}/questions/{{.ID}}/{{.Title}}</loc>\n  {{else}}\n    <loc>{{$.general.SiteUrl}}/questions/{{.ID}}</loc>\n  {{end}}\n    <lastmod>{{.UpdateTime}}</lastmod>\n  </url>\n  {{ end }}\n</urlset>\n"
  },
  {
    "path": "ui/template/sort-btns.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{define \"sort-btns\"}}\n<div role=\"group\" class=\"md-show me-2 btn-group btn-group-sm\">\n  <a role=\"button\" tabindex=\"0\" href=\"?order=newest\" class=\"text-capitalize fit-content btn active btn-outline-secondary\">\n    {{translator ($.language) \"ui.question.newest\" }}\n  </a>\n  <a role=\"button\" tabindex=\"0\" href=\"?order=active\" class=\"text-capitalize fit-content btn btn-outline-secondary\">\n    {{translator ($.language) \"ui.question.active\" }}\n  </a>\n  <!-- <a role=\"button\" tabindex=\"0\" href=\"?order=hot\" class=\"text-capitalize fit-content d-none d-md-block btn btn-outline-secondary\">\n    {{translator ($.language) \"ui.question.hot\" }}\n  </a> -->\n  <a role=\"button\" tabindex=\"0\" href=\"?order=unanswered\" class=\"text-capitalize fit-content d-none d-md-block btn btn-outline-secondary\" style=\"border-top-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem;\">\n    {{translator ($.language) \"ui.question.unanswered\" }}\n  </a>\n  <a role=\"button\" tabindex=\"0\" href=\"?order=frequent\" class=\"text-capitalize fit-content d-none d-md-block btn btn-outline-secondary\" style=\"border-top-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem;\">\n    {{translator ($.language) \"ui.question.frequent\" }}\n  </a>\n  <div role=\"group\" class=\"show dropdown btn-group\">\n    <button type=\"button\" aria-expanded=\"true\" class=\"dropdown-toggle show btn btn-outline-secondary btn-sm\">\n      {{translator ($.language) \"ui.btns.more\" }}\n    </button>\n    <div x-placement=\"bottom-start\" class=\"dropdown-menu show\" data-popper-reference-hidden=\"false\" data-popper-escaped=\"false\" data-popper-placement=\"bottom-start\" style=\"position: absolute; inset: 0px auto auto 0px; transform: translate(0px, 33px);\">\n      <a href=\"?order=score\" aria-selected=\"false\" class=\"text-capitalize dropdown-item\"> {{translator ($.language) \"ui.question.score\" }}</a>\n    </div>\n  </div>\n</div>\n\n<div class=\"dropdown\">\n  <button type=\"button\" id=\"react-aria8245013726-:r5:\" aria-expanded=\"false\" class=\"dropdown-toggle btn btn-outline-secondary btn-sm\"><i class=\"br bi-view-stacked\"></i></button>\n  <div x-placement=\"bottom-end\" aria-labelledby=\"react-aria8245013726-:r5:\" class=\"dropdown-menu dropdown-menu-end\" data-popper-reference-hidden=\"false\" data-popper-escaped=\"false\" data-popper-placement=\"bottom-end\" style=\"position: absolute; inset: 0px 0px auto auto; transform: translate(0px, 33px);\">\n    <h6 class=\"dropdown-header\" role=\"heading\">\n      {{translator ($.language) \"ui.btns.view\" }}\n    </h6>\n    <a aria-selected=\"true\" data-rr-ui-dropdown-item=\"\" class=\"dropdown-item active\" role=\"button\" tabindex=\"0\" href=\"#\" data-footnote-fixed=\"true\">\n      {{translator ($.language) \"ui.btns.card\" }}\n    </a>\n    <a aria-selected=\"false\" data-rr-ui-dropdown-item=\"\" class=\"dropdown-item\" role=\"button\" tabindex=\"0\" href=\"#\" data-footnote-fixed=\"true\">\n      {{translator ($.language) \"ui.btns.compact\" }}\n    </a>\n  </div>\n</div>\n{{end}}\n"
  },
  {
    "path": "ui/template/tag-detail.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n<div class=\"d-flex justify-content-center px-0 px-md-4\">\n  <div class=\"answer-container\">\n    <div class=\"pt-4 mb-5 row\">\n      <div class=\"page-main flex-auto col\">\n        <div class=\"tag-box mb-5\">\n          <h3 class=\"mb-3\">\n            <a class=\"link-dark\" href=\"{{$.baseURL}}/tags/{{$.tag.SlugName}}\">{{$.tag.SlugName}}</a>\n          </h3>\n          <p class=\"text-break\">{{formatLinkNofollow $.tag.ParsedText}}</p>\n        </div>\n        <div>\n          <div class=\"mb-3 d-flex flex-wrap justify-content-between\">\n            <h5 class=\"fs-5 text-nowrap mb-3 mb-md-0\">\n              {{translator ($.language) \"ui.question.x_questions\" \"count\"\n              .questionCount}}\n            </h5>\n            {{template \"sort-btns\" .}}\n          </div>\n          <div class=\"rounded list-group\">\n            {{range .questionList}}\n            <div class=\"bg-transparent py-3 px-0 border-start-0 border-end-0 list-group-item\">\n              <h5 class=\"text-wrap text-break\">\n                {{if $.useTitle }}\n                <a class=\"link-dark\" href=\"{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}\">{{.Title}}</a>\n                {{else}}\n                <a class=\"link-dark\" href=\"{{$.baseURL}}/questions/{{.ID}}\">{{.Title}}</a>\n                {{end}}\n              </h5>\n              <div class=\"d-flex flex-wrap flex-column flex-md-row align-items-md-center small mb-2 text-secondary\">\n                <div class=\"d-flex flex-wrap me-0 me-md-3\">\n                  <div class=\"d-flex align-items-center text-secondary me-1\">\n                    <a href=\"{{$.baseURL}}/users/{{.Operator.Username}}\"><span\n                        class=\"me-1 text-break\">{{.Operator.DisplayName}}</span></a><span class=\"fw-bold\"\n                      title=\"Reputation\">{{.Operator.Rank}}</span>\n                  </div>\n                  •\n                  <time class=\"text-secondary ms-1\" datetime=\"{{timeFormatISO $.timezone .OperatedAt}}\"\n                    title=\"{{translatorTimeFormatLongDate $.language $.timezone .OperatedAt}}\">{{translator $.language\n                    \"ui.question.asked\"}}\n                    {{translatorTimeFormat $.language $.timezone .OperatedAt}}\n                  </time>\n                </div>\n                <div class=\"d-flex align-items-center mt-2 mt-md-0\">\n                  <div class=\"d-flex align-items-center flex-shrink-0\">\n                    <i class=\"br bi-hand-thumbs-up-fill\"></i>\n                    <em class=\"fst-normal ms-1\">{{.VoteCount}}</em>\n                  </div>\n                  <div class=\"d-flex flex-shrink-0 align-items-center ms-3\">\n                    <i class=\"br bi-chat-square-text-fill\"></i>\n                    <em class=\"fst-normal ms-1\">{{.AnswerCount}}</em>\n                  </div>\n                  <span class=\"summary-stat ms-3 flex-shrink-0\">\n                    <i class=\"br bi-bar-chart-fill\"></i>\n                    <em class=\"fst-normal ms-1\">{{.ViewCount}}</em>\n                  </span>\n                </div>\n              </div>\n              <div class=\"question-tags mx-n1\">\n                {{range .Tags }}\n                <a href=\"{{$.baseURL}}/tags/{{.SlugName}}\"\n                  class=\"badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1\">\n                  <span class=\"\">{{.SlugName}}</span>\n                </a>\n                {{end}}\n              </div>\n            </div>\n            {{end}}\n          </div>\n          <div class=\"mt-4 mb-2 d-flex justify-content-center\">\n            {{template \"page\" .}}\n          </div>\n        </div>\n      </div>\n      <div class=\"page-right-side mt-4 mt-xl-0 col\">\n        {{template \"hot-question\" .}}\n      </div>\n    </div>\n  </div>\n</div>\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/template/tags.html",
    "content": "<!--\n\n    Licensed to the Apache Software Foundation (ASF) under one\n    or more contributor license agreements.  See the NOTICE file\n    distributed with this work for additional information\n    regarding copyright ownership.  The ASF licenses this file\n    to you under the Apache License, Version 2.0 (the\n    \"License\"); you may not use this file except in compliance\n    with the License.  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n\n-->\n{{template \"header\" . }}\n<div class=\"d-flex justify-content-center px-0 px-md-4\">\n  <div class=\"answer-container\">\n    <div class=\"py-4 mb-4 row\">\n      <div class=\"col-xxl-12\">\n        <h3 class=\"mb-4\">\n          {{translator $.language \"ui.page_title.tags\"}}\n        </h3>\n        <div class=\"d-block d-sm-flex justify-content-between align-items-center flex-wrap\">\n          <div class=\"mb-3 mb-sm-0 hstack gap-3\">\n            <form class=\"\">\n              <div>\n                <input placeholder=\"{{translator $.language \"ui.tags.search_placeholder\"}}\" type=\"search\" id=\"formBasicEmail\" class=\"form-control form-control-sm\" value=\"\">\n              </div>\n            </form>\n          </div>\n          <div role=\"group\" class=\"btn-group btn-group-sm\">\n            <a role=\"button\" tabindex=\"0\" href=\"?sort=popular\" class=\"text-capitalize fit-content btn active btn-outline-secondary\">\n              {{translator $.language \"ui.tags.sort_buttons.popular\"}}\n            </a>\n            <a role=\"button\" tabindex=\"0\" href=\"?sort=name\" class=\"text-capitalize fit-content btn btn-outline-secondary\">\n              {{translator $.language \"ui.tags.sort_buttons.name\"}}\n            </a>\n            <a role=\"button\" tabindex=\"0\" href=\"?sort=newest\" class=\"text-capitalize fit-content btn btn-outline-secondary\">\n              {{translator $.language \"ui.tags.sort_buttons.newest\"}}\n            </a>\n          </div>\n        </div>\n      </div>\n      <div class=\"mt-4 col-sm-12\">\n        <div class=\"row\">\n          {{ range .data.List }}\n          <div class=\"mb-4 col-lg-3 col-md-4 col-sm-6 col-12\">\n            <div class=\"h-100 card\">\n              <div class=\"d-flex flex-column align-items-start card-body\">\n                <a href=\"{{$.baseURL}}/tags/{{.SlugName}}\" class=\"badge-tag rounded-1 mb-3\"><span\n                    class=\"\">{{.SlugName}}</span></a>\n                <div class=\"small flex-fill text-break text-wrap text-truncate-3 reset-p mb-3\">  {{formatLinkNofollow .ParsedText}}\n                </div>\n                <div class=\"d-flex align-items-center\">\n                  <span class=\"text-secondary small text-nowrap\">{{.QuestionCount}}\n                    {{translator $.language \"ui.tags.tag_label\"}}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n          {{ end }}\n\n        </div>\n        <div class=\"d-flex justify-content-center\">\n          {{template \"page\" .}}\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n{{template \"footer\" .}}\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noImplicitAny\": false,\n    \"ignoreDeprecations\": \"5.0\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ],\n      \"@i18n/*\": [\n        \"../i18n/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src\",\n    \"node_modules/@testing-library/jest-dom\",\n    \"scripts\"\n  ],\n  \"exclude\": [\n    \"src/plugins\"\n  ]\n}\n"
  }
]